├── CH02
├── .gitignore
├── LICENSE
├── README.md
├── activator
├── activator-launch-1.2.7.jar
├── activator.bat
├── app
│ ├── actors
│ │ └── TwitterStreamer.scala
│ ├── controllers
│ │ └── Application.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── main.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ └── plugins.sbt
└── public
│ ├── images
│ └── favicon.png
│ ├── javascripts
│ └── hello.js
│ └── stylesheets
│ └── main.css
├── CH03
├── .gitignore
├── README.md
├── build.sbt
├── play
│ └── app
│ │ ├── controllers
│ │ └── Application.scala
│ │ └── models
│ │ └── User.scala
├── project
│ └── plugins.sbt
└── src
│ └── main
│ ├── java
│ ├── Partition.java
│ ├── Statement.java
│ └── User.java
│ └── scala
│ ├── ForComprehension.scala
│ ├── Match.scala
│ ├── Reporting.scala
│ └── ScalaPartition.scala
├── CH04
├── .gitignore
├── app
│ ├── ErrorHandler.scala
│ ├── Filters.scala
│ ├── binders
│ │ ├── PathBinders.scala
│ │ └── QueryStringBinders.scala
│ ├── controllers
│ │ ├── Import.scala
│ │ └── Quiz.scala
│ ├── filters
│ │ └── ScoreFilter.scala
│ ├── models
│ │ └── Vocabulary.scala
│ └── services
│ │ └── VocabularyService.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
└── project
│ ├── build.properties
│ └── plugins.sbt
├── CH05
├── .gitignore
├── app
│ ├── controllers
│ │ └── Application.scala
│ ├── services
│ │ ├── AuthenticationService.scala
│ │ ├── StatisticsRepository.scala
│ │ ├── StatisticsService.scala
│ │ └── TwitterService.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── main.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ ├── play.plugins
│ ├── routes
│ └── twitter.conf
├── project
│ ├── build.properties
│ └── plugins.sbt
├── public
│ ├── images
│ │ └── favicon.png
│ ├── javascripts
│ │ └── jquery-1.9.0.min.js
│ └── stylesheets
│ │ └── main.css
└── test
│ └── services
│ ├── AuthenticationServiceSpec.scala
│ └── StatisticsServiceSpec.scala
├── CH06
├── .gitignore
├── app
│ ├── actors
│ │ ├── StatisticsProvider.scala
│ │ ├── Storage.scala
│ │ ├── TweetReachComputer.scala
│ │ ├── TwitterCredentials.scala
│ │ └── UserFollowersCounter.scala
│ ├── controllers
│ │ └── Application.scala
│ ├── messages
│ │ └── Messages.scala
│ └── modules
│ │ └── Actors.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ ├── routes
│ └── twitter.conf
├── project
│ ├── build.properties
│ └── plugins.sbt
└── public
│ ├── images
│ └── favicon.png
│ ├── javascripts
│ └── jquery-1.9.0.min.js
│ └── stylesheets
│ └── main.css
├── CH07
├── .gitignore
├── README.md
├── app
│ ├── actors
│ │ ├── CQRSCommandHandler.scala
│ │ ├── CQRSEventHandler.scala
│ │ ├── CQRSQueryHandler.scala
│ │ ├── ClientCommandHandler.scala
│ │ ├── Messages.scala
│ │ ├── SMSHandler.scala
│ │ ├── SMSServer.scala
│ │ └── SMSService.scala
│ ├── controllers
│ │ └── Application.scala
│ ├── generated
│ │ ├── Keys.scala
│ │ ├── Public.scala
│ │ ├── Sequences.scala
│ │ ├── Tables.scala
│ │ └── tables
│ │ │ ├── MentionSubscriptions.scala
│ │ │ ├── Mentions.scala
│ │ │ ├── PlayEvolutions.scala
│ │ │ ├── TwitterUser.scala
│ │ │ ├── User.scala
│ │ │ └── records
│ │ │ ├── MentionSubscriptionsRecord.scala
│ │ │ ├── MentionsRecord.scala
│ │ │ ├── PlayEvolutionsRecord.scala
│ │ │ ├── TwitterUserRecord.scala
│ │ │ └── UserRecord.scala
│ ├── helpers
│ │ ├── Contexts.scala
│ │ ├── Database.scala
│ │ └── TwitterCredentials.scala
│ ├── modules
│ │ └── Fixtures.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── login.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── chapter7.xml
│ ├── evolutions
│ │ └── default
│ │ │ ├── 1.sql
│ │ │ └── 2.sql
│ ├── logback.xml
│ ├── routes
│ └── twitter.conf
└── project
│ ├── build.properties
│ └── plugins.sbt
├── CH08
├── .gitignore
├── app
│ ├── controllers
│ │ └── Application.scala
│ ├── generated
│ │ ├── Keys.scala
│ │ ├── Public.scala
│ │ ├── Sequences.scala
│ │ ├── Tables.scala
│ │ └── tables
│ │ │ ├── MentionSubscriptions.scala
│ │ │ ├── Mentions.scala
│ │ │ ├── PlayEvolutions.scala
│ │ │ ├── TwitterUser.scala
│ │ │ ├── User.scala
│ │ │ └── records
│ │ │ ├── MentionSubscriptionsRecord.scala
│ │ │ ├── MentionsRecord.scala
│ │ │ ├── PlayEvolutionsRecord.scala
│ │ │ ├── TwitterUserRecord.scala
│ │ │ └── UserRecord.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── main.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── chapter7.xml
│ ├── logback.xml
│ └── routes
├── modules
│ └── client
│ │ └── src
│ │ ├── main
│ │ ├── public
│ │ │ └── partials
│ │ │ │ └── dashboard.html
│ │ └── scala
│ │ │ └── dashboard
│ │ │ ├── DashboardApp.scala
│ │ │ ├── DashboardCtrl.scala
│ │ │ ├── GraphDataService.scala
│ │ │ ├── GrowlService.scala
│ │ │ └── WebsocketService.scala
│ │ └── test
│ │ └── scala
│ │ └── services
│ │ └── GraphDataServiceSuite.scala
├── project
│ └── plugins.sbt
└── public
│ ├── images
│ └── favicon.png
│ └── stylesheets
│ └── main.css
├── CH09
├── .gitignore
├── app
│ ├── controllers
│ │ └── Application.scala
│ ├── services
│ │ └── TwitterStreamService.scala
│ └── views
│ │ └── index.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ ├── routes
│ └── twitter.conf
└── project
│ ├── build.properties
│ └── plugins.sbt
├── CH10
├── .gitignore
├── README
├── app
│ ├── assets
│ │ └── javascripts
│ │ │ └── application.js
│ ├── controllers
│ │ └── Application.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── main.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ └── plugins.sbt
├── public
│ ├── images
│ │ └── favicon.png
│ ├── javascripts
│ │ └── hello.js
│ └── stylesheets
│ │ └── main.css
└── test
│ └── ApplicationSpec.scala
├── CH11
├── .gitignore
├── app
│ ├── actors
│ │ ├── RandomNumberComputer.scala
│ │ └── RandomNumberFetcher.scala
│ ├── controllers
│ │ └── Application.scala
│ ├── services
│ │ ├── DiceService.scala
│ │ └── RandomNumberService.scala
│ └── views
│ │ ├── index.scala.html
│ │ └── main.scala.html
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ └── plugins.sbt
├── public
│ ├── images
│ │ └── favicon.png
│ ├── javascripts
│ │ └── hello.js
│ └── stylesheets
│ │ └── main.css
└── test
│ ├── actors
│ └── RandomNumberComputerSpec.scala
│ └── services
│ └── DiceDrivenRandomNumberServiceSpec.scala
├── README.md
└── listings
├── CH02.md
├── CH04.md
├── CH05.md
├── CH06.md
├── CH07.md
├── CH08.md
├── CH09.md
├── CH10.md
└── CH11.md
/CH02/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 |
--------------------------------------------------------------------------------
/CH02/LICENSE:
--------------------------------------------------------------------------------
1 | This software is licensed under the Apache 2 license, quoted below.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with
4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
5 |
6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
8 | language governing permissions and limitations under the License.
--------------------------------------------------------------------------------
/CH02/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 2
2 |
3 | These are the examples of the second chapter of the book "Reactive Web-Applications with Play"
4 |
5 | You can run the example application with
6 |
7 | ./activator run
8 |
9 | In order to run this example you will need to get Twitter application codes at https://apps.twitter.com/ and replace them in `conf/application.conf`
10 |
--------------------------------------------------------------------------------
/CH02/activator-launch-1.2.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH02/activator-launch-1.2.7.jar
--------------------------------------------------------------------------------
/CH02/app/actors/TwitterStreamer.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor._
4 | import play.api._
5 | import play.api.Play.current
6 | import play.api.libs.iteratee._
7 | import play.api.libs.iteratee.Concurrent.Broadcaster
8 | import play.api.libs.json._
9 | import play.api.libs.oauth._
10 | import play.api.libs.ws.WS
11 | import play.extras.iteratees._
12 | import play.api.libs.concurrent.Execution.Implicits._
13 |
14 | import scala.collection.mutable.ArrayBuffer
15 |
16 | class TwitterStreamer(out: ActorRef) extends Actor {
17 | def receive = {
18 | case "subscribe" =>
19 | Logger.info("Received subscription from a client")
20 | TwitterStreamer.subscribe(out)
21 | }
22 |
23 | override def postStop() {
24 | Logger.info("Client unsubscribing from stream")
25 | TwitterStreamer.unsubscribe(out)
26 | }
27 | }
28 |
29 | object TwitterStreamer {
30 |
31 | private var broadcastEnumerator: Option[Enumerator[JsObject]] = None
32 |
33 | private var broadcaster: Option[Broadcaster] = None
34 |
35 | private val subscribers = new ArrayBuffer[ActorRef]()
36 |
37 | def props(out: ActorRef) = Props(new TwitterStreamer(out))
38 |
39 | def subscribe(out: ActorRef): Unit = {
40 |
41 | if (broadcastEnumerator.isEmpty) {
42 | init()
43 | }
44 |
45 | def twitterClient: Iteratee[JsObject, Unit] = Cont {
46 | case in@Input.EOF => Done(None)
47 | case in@Input.El(o) =>
48 | if (subscribers.contains(out)) {
49 | out ! o
50 | twitterClient
51 | } else {
52 | Done(None)
53 | }
54 | case in@Input.Empty =>
55 | twitterClient
56 | }
57 |
58 | broadcastEnumerator.foreach { enumerator =>
59 | enumerator run twitterClient
60 | }
61 | subscribers += out
62 | }
63 |
64 | def unsubscribe(subscriber: ActorRef): Unit = {
65 | val index = subscribers.indexWhere(_ == subscriber)
66 | if (index > 0) {
67 | subscribers.remove(index)
68 | Logger.info("Unsubscribed client from stream")
69 | }
70 | }
71 |
72 | def subscribeNode: Enumerator[JsObject] = {
73 | if (broadcastEnumerator.isEmpty) {
74 | TwitterStreamer.init()
75 | }
76 |
77 | broadcastEnumerator.getOrElse {
78 | Enumerator.empty[JsObject]
79 | }
80 | }
81 |
82 | def init(): Unit = {
83 |
84 | credentials.map { case (consumerKey, requestToken) =>
85 |
86 | val (iteratee, enumerator) = Concurrent.joined[Array[Byte]]
87 |
88 | val jsonStream: Enumerator[JsObject] = enumerator &>
89 | Encoding.decode() &>
90 | Enumeratee.grouped(JsonIteratees.jsSimpleObject)
91 |
92 | val (e, b) = Concurrent.broadcast(jsonStream)
93 |
94 | broadcastEnumerator = Some(e)
95 | broadcaster = Some(b)
96 |
97 | val maybeMasterNodeUrl = Option(System.getProperty("masterNodeUrl"))
98 | val url = maybeMasterNodeUrl.getOrElse {
99 | "https://stream.twitter.com/1.1/statuses/filter.json"
100 | }
101 |
102 | WS
103 | .url(url)
104 | .sign(OAuthCalculator(consumerKey, requestToken))
105 | .withQueryString("track" -> "cat")
106 | .get { response =>
107 | Logger.info("Status: " + response.status)
108 | iteratee
109 | }.map { _ =>
110 | Logger.info("Twitter stream closed")
111 | }
112 |
113 | } getOrElse {
114 | Logger.error("Twitter credentials are not configured")
115 | }
116 |
117 | }
118 |
119 | private def credentials = for {
120 | apiKey <- Play.configuration.getString("twitter.apiKey")
121 | apiSecret <- Play.configuration.getString("twitter.apiSecret")
122 | token <- Play.configuration.getString("twitter.token")
123 | tokenSecret <- Play.configuration.getString("twitter.tokenSecret")
124 | } yield (ConsumerKey(apiKey, apiSecret), RequestToken(token, tokenSecret))
125 |
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/CH02/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import actors.TwitterStreamer
4 | import play.api.Play.current
5 | import play.api.libs.json._
6 | import play.api.mvc._
7 |
8 | class Application extends Controller {
9 |
10 | def index = Action { implicit request =>
11 | Ok(views.html.index("Tweets"))
12 | }
13 |
14 | def tweets = WebSocket.acceptWithActor[String, JsValue] { request => out =>
15 | TwitterStreamer.props(out)
16 | }
17 |
18 | def replicateFeed = Action { implicit request =>
19 | Ok.feed(TwitterStreamer.subscribeNode)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/CH02/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(message: String)(implicit request: RequestHeader)
2 |
3 | @main(message) {
4 |
5 |
6 |
7 |
42 |
43 | }
--------------------------------------------------------------------------------
/CH02/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)
2 |
3 |
4 |
5 |
6 |
7 | @title
8 |
9 |
10 |
11 |
12 |
13 | @content
14 |
15 |
16 |
--------------------------------------------------------------------------------
/CH02/build.sbt:
--------------------------------------------------------------------------------
1 | name := """twitter-stream"""
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 | lazy val root = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.6"
8 |
9 | libraryDependencies ++= Seq(
10 | jdbc,
11 | cache,
12 | ws,
13 | "com.typesafe.play.extras" %% "iteratees-extras" % "1.5.0",
14 | specs2 % Test
15 | )
16 |
17 | resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"
18 |
19 | resolvers += "Typesafe private" at "https://private-repo.typesafe.com/typesafe/maven-releases"
20 |
21 | // Play provides two styles of routers, one expects its actions to be injected, the
22 | // other, legacy style, accesses its actions statically.
23 | routesGenerator := InjectedRoutesGenerator
24 |
25 | libraryDependencies += "com.ning" % "async-http-client" % "1.9.29"
26 |
--------------------------------------------------------------------------------
/CH02/conf/application.conf:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for the application.
2 | # ~~~~~
3 |
4 | # Secret key
5 | # ~~~~~
6 | # The secret key is used to secure cryptographics functions.
7 | #
8 | # This must be changed for production, but we recommend not changing it in this file.
9 | #
10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details.
11 | play.crypto.secret = "changeme"
12 |
13 | # The application languages
14 | # ~~~~~
15 | play.i18n.langs = [ "en" ]
16 |
17 | # Router
18 | # ~~~~~
19 | # Define the Router object to use for this application.
20 | # This router will be looked up first when the application is starting up,
21 | # so make sure this is the entry point.
22 | # Furthermore, it's assumed your route file is named properly.
23 | # So for an application router like `my.application.Router`,
24 | # you may need to define a router file `conf/my.application.routes`.
25 | # Default to Routes in the root package (and conf/routes)
26 | # play.http.router = my.application.Routes
27 |
28 | # Database configuration
29 | # ~~~~~
30 | # You can declare as many datasources as you want.
31 | # By convention, the default datasource is named `default`
32 | #
33 | # db.default.driver=org.h2.Driver
34 | # db.default.url="jdbc:h2:mem:play"
35 | # db.default.user=sa
36 | # db.default.password=""
37 |
38 | # Evolutions
39 | # ~~~~~
40 | # You can disable evolutions if needed
41 | # play.evolutions.enabled=false
42 |
43 | # You can disable evolutions for a specific datasource if necessary
44 | # play.evolutions.db.default.enabled=false
45 |
46 | # Twitter
47 | twitter.apiKey=""
48 | twitter.apiSecret=""
49 | twitter.token=""
50 | twitter.tokenSecret=""
--------------------------------------------------------------------------------
/CH02/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CH02/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 | GET /tweets controllers.Application.tweets
8 | GET /replicatedFeed controllers.Application.replicateFeed
9 |
10 | # Map static resources from the /public folder to the /assets URL path
11 | GET /assets/*file controllers.Assets.at(path="/public", file)
12 |
--------------------------------------------------------------------------------
/CH02/project/build.properties:
--------------------------------------------------------------------------------
1 | #Activator-generated Properties
2 | #Thu Jul 02 15:21:14 CEST 2015
3 | template.uuid=49a31253-85ed-4c4f-9041-f2f030591a8d
4 | sbt.version=0.13.9
5 |
--------------------------------------------------------------------------------
/CH02/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
3 |
4 | // web plugins
5 |
6 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
7 |
8 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.6")
9 |
10 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3")
11 |
12 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7")
13 |
14 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")
15 |
16 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0")
17 |
--------------------------------------------------------------------------------
/CH02/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH02/public/images/favicon.png
--------------------------------------------------------------------------------
/CH02/public/javascripts/hello.js:
--------------------------------------------------------------------------------
1 | if (window.console) {
2 | console.log("Welcome to your Play application's JavaScript!");
3 | }
--------------------------------------------------------------------------------
/CH02/public/stylesheets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH02/public/stylesheets/main.css
--------------------------------------------------------------------------------
/CH03/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 |
--------------------------------------------------------------------------------
/CH03/README.md:
--------------------------------------------------------------------------------
1 | # Chapter 3
2 |
3 | These are the examples of the third chapter of the book "Reactive Web-Applications with Play"
4 |
5 | The sources themselves do not make up a complete application but you can inspect each of them individually and play with them.
--------------------------------------------------------------------------------
/CH03/build.sbt:
--------------------------------------------------------------------------------
1 | name := "reactive-play-chapter-3"
2 |
3 | version := "1.0"
4 |
5 | libraryDependencies += "joda-time" % "joda-time" % "2.7"
6 |
7 | lazy val main = (project in file(".")).aggregate(play)
8 |
9 | lazy val play = (project in file("play")).enablePlugins(PlayScala)
10 |
--------------------------------------------------------------------------------
/CH03/play/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | import play.api._
2 | import play.api.mvc._
3 | import play.api.libs.json._
4 |
5 | import models._
6 |
7 | object Application extends Controller {
8 |
9 | def updateFirstName(userId: Long) = Action { implicit request =>
10 | val update: Option[Result] = for {
11 | json <- request.body.asJson
12 | user <- User.findOneById(userId)
13 | newFirstName <- (json \ "firstName").asOpt[String]
14 | if !newFirstName.trim.isEmpty
15 | } yield {
16 | User.updateFirstName(user.id, newFirstName)
17 | Ok
18 | }
19 |
20 | update.getOrElse {
21 | BadRequest(Json.obj("error" -> "Could not update your first name, please make sure that it is not empty"))
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/CH03/play/app/models/User.scala:
--------------------------------------------------------------------------------
1 | case class User(id: Long, firstName: String, lastName: String)
2 |
3 | object User {
4 | def findOneById(id: Long): Option[User] = None
5 | def updateFirstName(id: Long, newFirstName: String) = ()
6 | }
7 |
--------------------------------------------------------------------------------
/CH03/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
2 |
--------------------------------------------------------------------------------
/CH03/src/main/java/Partition.java:
--------------------------------------------------------------------------------
1 | import java.util.ArrayList;
2 | import java.util.LinkedList;
3 | import java.util.List;
4 |
5 | public class Partition {
6 |
7 | public static void main(String... args) {
8 |
9 | User bob = new User("Bob", "Marley", 19);
10 | User jimmy = new User("Jimmy", "Hendrix", 16);
11 |
12 | List users = new LinkedList<>();
13 | users.add(bob);
14 | users.add(jimmy);
15 |
16 | List minors = new ArrayList();
17 | List majors = new ArrayList();
18 |
19 | for(int i = 0; i < users.size(); i++) {
20 | User u = users.get(i);
21 | if(u.getAge() < 18) {
22 | minors.add(u);
23 | } else {
24 | majors.add(u);
25 | }
26 | }
27 | }
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/CH03/src/main/java/Statement.java:
--------------------------------------------------------------------------------
1 | import java.util.LinkedList;
2 | import java.util.List;
3 |
4 | public class Statement {
5 |
6 | /**
7 | * Statement that mutates the original list
8 | * @param list the list to be filtered
9 | * @param toRemove the String to remove
10 | */
11 | public static void removeElement(List list, String toRemove) {
12 | int index = 0;
13 | for (String s : list) {
14 | if (s.equals(toRemove)) {
15 | list.remove(index);
16 | }
17 | index++;
18 | }
19 | }
20 |
21 | /**
22 | * Expression that returns a new list without altering the original one
23 | * @param list the list to be filtered
24 | * @param toRemove the String to be removed
25 | * @return a new list that does not contain the String to be removed
26 | */
27 | public static List filterNot(List list, String toRemove) {
28 | List filtered = new LinkedList();
29 | for (String s : list) {
30 | if (!s.equals(toRemove)) {
31 | filtered.add(s);
32 | }
33 | }
34 | return filtered;
35 | }
36 |
37 |
38 | }
--------------------------------------------------------------------------------
/CH03/src/main/java/User.java:
--------------------------------------------------------------------------------
1 | public class User {
2 |
3 | private String firstName;
4 |
5 | private String lastName;
6 |
7 | private Integer age;
8 |
9 | public String getFirstName() {
10 | return firstName;
11 | }
12 |
13 | public String getLastName() {
14 | return lastName;
15 | }
16 |
17 | public Integer getAge() {
18 | return age;
19 | }
20 |
21 | public User(String firstName, String lastName, Integer age) {
22 | this.firstName = firstName;
23 | this.lastName = lastName;
24 | this.age = age;
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/CH03/src/main/scala/ForComprehension.scala:
--------------------------------------------------------------------------------
1 | class ForComprehension {
2 |
3 | val aList = List(1, 2, 3)
4 | val bList = List(4, 5, 6)
5 |
6 | val result = for {
7 | a <- aList
8 | if a > 1
9 | b <- bList
10 | if b < 6
11 | } yield a + b
12 |
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/CH03/src/main/scala/Match.scala:
--------------------------------------------------------------------------------
1 | object Match {
2 |
3 | def spellOut(number: Int): String = number match {
4 | case 1 => "one"
5 | case 2 => "two"
6 | case 42 => "forty-two"
7 | case _ => "unknown number"
8 | }
9 |
10 | spellOut(42)
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/CH03/src/main/scala/Reporting.scala:
--------------------------------------------------------------------------------
1 | import org.joda.time.DateTime
2 |
3 | case class Click(timestamp: DateTime, advertisementId: Long)
4 |
5 | case class Month(year: Int, month: Int)
6 |
7 | trait ClickRepository {
8 |
9 | def getClicksSince(when: DateTime): List[Click]
10 | }
11 |
12 | object Reporting {
13 |
14 | def computeYearlyAggregates(clickRepository: ClickRepository): Map[Long, Seq[(Month, Int)]] = {
15 | val pastClicks = clickRepository.getClicksSince(DateTime.now.minusYears(1))
16 | pastClicks.groupBy(_.advertisementId).mapValues {
17 | case clicks =>
18 | val monthlyClicks = clicks
19 | .groupBy(click => Month(click.timestamp.getYear, click.timestamp.getMonthOfYear))
20 | .map { case (month, groupedClicks) =>
21 | month -> groupedClicks.length
22 | }.toSeq
23 | monthlyClicks
24 | }
25 | }
26 |
27 | def computeYearlyAggregatesRefactored(clickRepository: ClickRepository): Map[Long, Seq[(Month, Int)]] = {
28 |
29 | def monthOfClick(click: Click) = Month(click.timestamp.getYear, click.timestamp.getMonthOfYear)
30 |
31 | def countMonthlyClicks(monthlyClicks: (Month, Seq[Click])) = monthlyClicks match {
32 | case (month, clicks) =>
33 | month -> clicks.length
34 | }
35 |
36 | def computeMonthlyAggregates(clicks: Seq[Click]) = clicks.groupBy(monthOfClick).map(countMonthlyClicks).toSeq
37 |
38 | val pastClicks = clickRepository.getClicksSince(DateTime.now.minusYears(1))
39 |
40 | pastClicks.groupBy(_.advertisementId).mapValues(computeMonthlyAggregates)
41 |
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/CH03/src/main/scala/ScalaPartition.scala:
--------------------------------------------------------------------------------
1 | object ScalaPartition {
2 |
3 | case class User(firstName: String, lastName: String, age: Int)
4 |
5 | def main(args: String*): Unit = {
6 |
7 | val users = List(
8 | User("Bob", "Marley", 19),
9 | User("Jimmy", "Hendrix", 16)
10 | )
11 |
12 | val (minors, majors) = users.partition(_.age < 18)
13 |
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/CH04/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 |
17 |
--------------------------------------------------------------------------------
/CH04/app/ErrorHandler.scala:
--------------------------------------------------------------------------------
1 | import javax.inject._
2 | import play.api.http.DefaultHttpErrorHandler
3 | import play.api._
4 | import play.api.mvc._
5 | import play.api.mvc.Results._
6 | import play.api.routing.Router
7 | import scala.concurrent._
8 |
9 | class ErrorHandler @Inject() (
10 | env: Environment,
11 | config: Configuration,
12 | sourceMapper: OptionalSourceMapper,
13 | router: Provider[Router])
14 | extends DefaultHttpErrorHandler(env, config, sourceMapper, router) {
15 |
16 | override protected def onNotFound(request: RequestHeader, message: String): Future[Result] = {
17 | Future.successful {
18 | NotFound("Could not find " + request)
19 | }
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/CH04/app/Filters.scala:
--------------------------------------------------------------------------------
1 | import javax.inject.Inject
2 |
3 | import filters.ScoreFilter
4 | import play.api.http.HttpFilters
5 | import play.filters.gzip.GzipFilter
6 | import play.filters.headers.SecurityHeadersFilter
7 |
8 | class Filters @Inject() (
9 | gzip: GzipFilter,
10 | score: ScoreFilter) extends HttpFilters {
11 |
12 | val filters = Seq(gzip, SecurityHeadersFilter(), score)
13 | }
--------------------------------------------------------------------------------
/CH04/app/binders/PathBinders.scala:
--------------------------------------------------------------------------------
1 | package binders
2 |
3 | import play.api.i18n.Lang
4 | import play.api.mvc.PathBindable
5 |
6 | object PathBinders {
7 |
8 | implicit object LangPathBindable extends PathBindable[Lang] {
9 | override def bind(key: String, value: String): Either[String, Lang] =
10 | Lang.get(value).toRight(s"Language $value is not recognized")
11 |
12 | override def unbind(key: String, value: Lang): String = value.code
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/CH04/app/binders/QueryStringBinders.scala:
--------------------------------------------------------------------------------
1 | package binders
2 |
3 | import play.api.i18n.Lang
4 | import play.api.mvc.QueryStringBindable
5 |
6 | object QueryStringBinders {
7 |
8 | implicit object LangQueryStringBinder extends QueryStringBindable[Lang] {
9 | override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Lang]] = {
10 | val code = params.get(key).flatMap(_.headOption)
11 | code.map { c =>
12 | Lang.get(c).toRight(s"$c is not a valid language")
13 | }
14 | }
15 |
16 | override def unbind(key: String, value: Lang): String = value.code
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/CH04/app/controllers/Import.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 |
5 | import models.Vocabulary
6 | import play.api.i18n.Lang
7 | import play.api.mvc._
8 | import services.VocabularyService
9 |
10 | class Import @Inject() (vocabulary: VocabularyService) extends Controller {
11 |
12 | def importWord(sourceLanguage: Lang, word: String, targetLanguage: Lang, translation: String) = Action {
13 | val added = vocabulary.addVocabulary(Vocabulary(sourceLanguage, targetLanguage, word, translation))
14 | if (added) Ok else Conflict
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/CH04/app/controllers/Quiz.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 |
5 | import akka.actor.{ Props, ActorRef, Actor }
6 | import play.api.Play.current
7 | import play.api.mvc._
8 | import play.api.i18n.Lang
9 | import services.VocabularyService
10 |
11 | class Quiz @Inject() (vocabulary: VocabularyService) extends Controller {
12 |
13 | def quiz(sourceLanguage: Lang, targetLanguage: Lang) = Action {
14 | vocabulary.findRandomVocabulary(sourceLanguage, targetLanguage).map { v =>
15 | Ok(v.word)
16 | } getOrElse {
17 | NotFound
18 | }
19 | }
20 |
21 | def check(sourceLanguage: Lang, word: String, targetLanguage: Lang, translation: String) = Action { request =>
22 | val isCorrect = vocabulary.verify(sourceLanguage, word, targetLanguage, translation)
23 | val correctScore: Int = request.session.get("correct").map(_.toInt).getOrElse(0)
24 | val wrongScore = request.session.get("wrong").map(_.toInt).getOrElse(0)
25 | if (isCorrect) {
26 | Ok.withSession("correct" -> (correctScore + 1).toString, "wrong" -> wrongScore.toString)
27 | } else {
28 | NotAcceptable.withSession("correct" -> correctScore.toString, "wrong" -> (wrongScore + 1).toString)
29 | }
30 | }
31 |
32 | def quizEndpoint(sourceLang: Lang, targetLang: Lang) = WebSocket.acceptWithActor[String, String] { request =>
33 | out =>
34 | QuizActor.props(out, sourceLang, targetLang, vocabulary)
35 | }
36 |
37 | }
38 |
39 | class QuizActor(out: ActorRef, sourceLang: Lang, targetLang: Lang, vocabulary: VocabularyService) extends Actor {
40 |
41 | private var word = ""
42 |
43 | override def preStart(): Unit = sendWord()
44 |
45 | def receive = {
46 | case translation: String if vocabulary.verify(sourceLang, word, targetLang, translation) =>
47 | out ! "Correct"
48 | sendWord()
49 | case _ => out ! "Incorrect, try again!"
50 | }
51 |
52 | def sendWord() = {
53 | vocabulary.findRandomVocabulary(sourceLang, targetLang).map { v =>
54 | out ! s"Please translate '${v.word}'"
55 | word = v.word
56 | } getOrElse {
57 | out ! s"I don't know any word for ${sourceLang.code} and ${targetLang.code}"
58 | }
59 | }
60 |
61 | }
62 |
63 | object QuizActor {
64 |
65 | def props(out: ActorRef, sourceLang: Lang, targetLang: Lang, vocabulary: VocabularyService): Props =
66 | Props(classOf[QuizActor], out, sourceLang, targetLang, vocabulary)
67 | }
--------------------------------------------------------------------------------
/CH04/app/filters/ScoreFilter.scala:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import play.api.libs.iteratee.Enumerator
4 | import play.api.mvc.{ Result, RequestHeader, Filter }
5 | import play.api.libs.concurrent.Execution.Implicits._
6 |
7 | import scala.concurrent.Future
8 |
9 | class ScoreFilter extends Filter {
10 | override def apply(
11 | nextFilter: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = {
12 |
13 | val result = nextFilter(rh)
14 | result.map { res =>
15 |
16 | if (res.header.status == 200 || res.header.status == 406) {
17 | val correct = res.session(rh).get("correct").getOrElse(0)
18 | val wrong = res.session(rh).get("wrong").getOrElse(0)
19 | val score = s"\n\nYour current score is: $correct correct answers and $wrong wrong answers"
20 | val newBody = res.body andThen Enumerator(score.getBytes("UTF-8"))
21 | res.copy(body = newBody)
22 | } else {
23 | res
24 | }
25 |
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/CH04/app/models/Vocabulary.scala:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import play.api.i18n.Lang
4 |
5 | case class Vocabulary(sourceLanguage: Lang, targetLanguage: Lang, word: String, translation: String)
--------------------------------------------------------------------------------
/CH04/app/services/VocabularyService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import javax.inject.Singleton
4 |
5 | import models.Vocabulary
6 | import play.api.i18n.Lang
7 |
8 | import scala.util.Random
9 |
10 | @Singleton
11 | class VocabularyService() {
12 |
13 | private var allVocabulary = List(
14 | Vocabulary(Lang("en"), Lang("fr"), "hello", "bonjour"),
15 | Vocabulary(Lang("en"), Lang("fr"), "play", "jouer")
16 | )
17 |
18 | def addVocabulary(v: Vocabulary): Boolean = {
19 | if (!allVocabulary.contains(v)) {
20 | allVocabulary = v :: allVocabulary
21 | true
22 | } else {
23 | false
24 | }
25 | }
26 |
27 | def findRandomVocabulary(sourceLanguage: Lang, targetLanguage: Lang): Option[Vocabulary] = {
28 | Random.shuffle(allVocabulary.filter { v =>
29 | v.sourceLanguage == sourceLanguage &&
30 | v.targetLanguage == targetLanguage
31 | }).headOption
32 | }
33 |
34 | def verify(sourceLanguage: Lang,
35 | word: String,
36 | targetLanguage: Lang,
37 | translation: String): Boolean = {
38 | allVocabulary.contains(
39 | Vocabulary(sourceLanguage, targetLanguage, word, translation)
40 | )
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/CH04/build.sbt:
--------------------------------------------------------------------------------
1 | name := "simple-vocabulary-teacher"
2 |
3 | version := "1.0"
4 |
5 | scalaVersion := "2.11.7"
6 |
7 | lazy val `simple-vocabulary-teacher` = (project in file(".")).enablePlugins(PlayScala)
8 |
9 | routesGenerator := InjectedRoutesGenerator
10 |
11 | com.typesafe.sbt.SbtScalariform.scalariformSettings
12 |
13 | routesImport += "binders.PathBinders._"
14 | routesImport += "binders.QueryStringBinders._"
15 |
16 | libraryDependencies += filters
17 |
18 |
--------------------------------------------------------------------------------
/CH04/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.crypto.secret="?pXoACTY?Wq5NUufmusmY_PUQmX1jj8vg3`IT[iA8FEE?grocq^asYuxR`7gXoj["
2 |
3 | play.i18n.langs = [ "en" ]
--------------------------------------------------------------------------------
/CH04/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
12 | ${application.home}/logs/application.log
13 |
14 |
15 | ${application.home}/../logs/application.%d{yyyy-MM-dd}.log
16 |
17 |
18 | 30
19 |
20 |
21 |
22 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/CH04/conf/routes:
--------------------------------------------------------------------------------
1 | PUT /import/word/:sourceLang/:word/:targetLang/:translation controllers.Import.importWord(sourceLang: play.api.i18n.Lang, word, targetLang: play.api.i18n.Lang, translation)
2 |
3 | GET /quizz/:sourceLang controllers.Quiz.quiz(sourceLang: play.api.i18n.Lang, targetLang: play.api.i18n.Lang)
4 | POST /quizz/:sourceLang/check/:word controllers.Quiz.check(sourceLang: play.api.i18n.Lang, word, targetLang: play.api.i18n.Lang, translation)
5 | GET /quizz/interactive/:sourceLang/:targetLang controllers.Quiz.quizEndpoint(sourceLang: play.api.i18n.Lang, targetLang: play.api.i18n.Lang)
--------------------------------------------------------------------------------
/CH04/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 0.13.9
2 |
--------------------------------------------------------------------------------
/CH04/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
2 |
3 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0")
4 |
--------------------------------------------------------------------------------
/CH05/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 | twitter.conf
--------------------------------------------------------------------------------
/CH05/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import play.api._
4 | import play.api.mvc._
5 |
6 | object Application extends Controller {
7 |
8 | def index = Action {
9 | Ok(views.html.index("Your new application is ready."))
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/CH05/app/services/AuthenticationService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import scala.concurrent.{ExecutionContext, Future}
4 |
5 | trait AuthenticationService {
6 |
7 | def authenticateUser(email: String, password: String)(implicit ec: ExecutionContext): Future[AuthenticationResult]
8 |
9 | }
10 |
11 | sealed trait AuthenticationResult
12 | case object AuthenticationSuccessful extends AuthenticationResult
13 | case object AuthenticationUnsuccessful extends AuthenticationResult
14 |
15 | /**
16 | * Dummy implementation aimed at demonstrating the testing of Futures
17 | */
18 | class DummyAuthenticationService extends AuthenticationService {
19 |
20 | override def authenticateUser(email: String, password: String)(implicit ec: ExecutionContext): Future[AuthenticationResult] = Future {
21 | email match {
22 | case "bob@marley.org" =>
23 | // never do this in reality. This is for testing purposes only!
24 | Thread.sleep(200)
25 | AuthenticationSuccessful
26 | case "jimmy@hendrix.com" =>
27 | // never do this in reality. This is for testing purposes only!
28 | Thread.sleep(500)
29 | throw new RuntimeException("Future timed out after 500ms")
30 | case _ =>
31 | AuthenticationUnsuccessful
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/CH05/app/services/StatisticsRepository.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import javax.inject.Inject
4 |
5 | import org.joda.time.DateTime
6 | import play.modules.reactivemongo._
7 | import reactivemongo.api.collections.bson.BSONCollection
8 | import reactivemongo.bson._
9 |
10 | import scala.concurrent.{ExecutionContext, Future}
11 | import scala.util.control.NonFatal
12 |
13 | trait StatisticsRepository {
14 |
15 | def storeCounts(counts: StoredCounts)(implicit ec: ExecutionContext): Future[Unit]
16 |
17 | def retrieveLatestCounts(userName: String)(implicit ec: ExecutionContext): Future[StoredCounts]
18 |
19 | }
20 |
21 | case class StoredCounts(when: DateTime, userName: String, followersCount: Long, friendsCount: Long)
22 |
23 | object StoredCounts {
24 | implicit object UserCountsReader extends BSONDocumentReader[StoredCounts] with BSONDocumentWriter[StoredCounts] {
25 | override def read(bson: BSONDocument): StoredCounts = {
26 | val when = bson.getAs[BSONDateTime]("when").map(t => new DateTime(t.value)).get
27 | val userName = bson.getAs[String]("userName").get
28 | val followersCount = bson.getAs[Long]("followersCount").get
29 | val friendsCount = bson.getAs[Long]("friendsCount").get
30 | StoredCounts(when, userName, followersCount, friendsCount)
31 | }
32 |
33 | override def write(t: StoredCounts): BSONDocument = BSONDocument(
34 | "when" -> BSONDateTime(t.when.getMillis),
35 | "userName" -> t.userName,
36 | "followersCount" -> t.followersCount,
37 | "friendsCount" -> t.friendsCount
38 | )
39 | }
40 | }
41 |
42 | class MongoStatisticsRepository @Inject() (reactiveMongo: ReactiveMongoApi) extends StatisticsRepository {
43 |
44 | private val StatisticsCollection = "UserStatistics"
45 |
46 | private lazy val collection = reactiveMongo.db.collection[BSONCollection](StatisticsCollection)
47 |
48 | override def storeCounts(counts: StoredCounts)(implicit ec: ExecutionContext): Future[Unit] = {
49 | collection.insert(counts).map { lastError =>
50 | if(lastError.inError) {
51 | throw CountStorageException(counts)
52 | }
53 | }
54 | }
55 |
56 | override def retrieveLatestCounts(userName: String)(implicit ec: ExecutionContext): Future[StoredCounts] = {
57 | val query = BSONDocument("userName" -> userName)
58 | val order = BSONDocument("_id" -> -1)
59 | collection
60 | .find(query)
61 | .sort(order)
62 | .one[StoredCounts]
63 | .map { counts => counts getOrElse StoredCounts(DateTime.now, userName, 0, 0) }
64 | } recover {
65 | case NonFatal(t) =>
66 | throw CountRetrievalException(userName, t)
67 | }
68 | }
69 |
70 | case class CountRetrievalException(userName: String, cause: Throwable) extends RuntimeException("Could not read counts for " + userName, cause)
71 | case class CountStorageException(counts: StoredCounts) extends RuntimeException
--------------------------------------------------------------------------------
/CH05/app/services/StatisticsService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import org.joda.time.{Period, DateTime}
4 |
5 | import scala.concurrent.{ExecutionContext, Future}
6 | import scala.util.control.NonFatal
7 |
8 | trait StatisticsService {
9 |
10 | def createUserStatistics(userName: String)(implicit ec: ExecutionContext): Future[Unit]
11 |
12 | }
13 |
14 | class DefaultStatisticsService(statisticsRepository: StatisticsRepository, twitterService: TwitterService) extends StatisticsService {
15 |
16 | override def createUserStatistics(userName: String)(implicit ec: ExecutionContext): Future[Unit] = {
17 |
18 | def storeCounts(counts: (StoredCounts, TwitterCounts)): Future[Unit] = counts match { case (previous, current) =>
19 | statisticsRepository.storeCounts(StoredCounts(DateTime.now, userName, current.followersCount, current.friendsCount))
20 | }
21 |
22 | def publishMessage(counts: (StoredCounts, TwitterCounts)): Future[Unit] = counts match { case (previous, current) =>
23 | val followersDifference = current.followersCount - previous.followersCount
24 | val friendsDifference = current.friendsCount - previous.friendsCount
25 | def phrasing(difference: Long) = if (difference >= 0) "gained" else "lost"
26 | val durationInDays = new Period(previous.when, DateTime.now).getDays
27 |
28 | twitterService.postTweet(
29 | s"@$userName in the past $durationInDays you have " +
30 | s"${phrasing(followersDifference)} $followersDifference " +
31 | s"followers and ${phrasing(followersDifference)} " +
32 | s"$friendsDifference friends"
33 | )
34 | }
35 |
36 |
37 | // first group of steps: retrieving previous and current counts
38 | val previousCounts: Future[StoredCounts] = statisticsRepository.retrieveLatestCounts(userName)
39 | val currentCounts: Future[TwitterCounts] = twitterService.fetchRelationshipCounts(userName)
40 |
41 | val counts: Future[(StoredCounts, TwitterCounts)] = for {
42 | previous <- previousCounts
43 | current <- currentCounts
44 | } yield {
45 | (previous, current)
46 | }
47 |
48 | // second group of steps: using the counts in order to store them and publish a message on Twitter
49 | val storedCounts: Future[Unit] = counts.flatMap(storeCounts)
50 | val publishedMessage: Future[Unit] = counts.flatMap(publishMessage)
51 |
52 | val result = for {
53 | _ <- storedCounts
54 | _ <- publishedMessage
55 | } yield {}
56 |
57 | result recoverWith {
58 | case CountStorageException(countsToStore) =>
59 | retryStoring(countsToStore, attemptNumber = 0)
60 | } recover {
61 | case CountStorageException(countsToStore) =>
62 | throw StatisticsServiceFailed("We couldn't save the statistics to our database. Next time it will work!")
63 | case CountRetrievalException(user, cause) =>
64 | throw StatisticsServiceFailed("We have a problem with our database. Sorry!", cause)
65 | case TwitterServiceException(message) =>
66 | throw StatisticsServiceFailed(s"We have a problem contacting Twitter: $message")
67 | case NonFatal(t) =>
68 | throw StatisticsServiceFailed("We have an unknown problem. Sorry!", t)
69 | }
70 |
71 | }
72 |
73 | private def retryStoring(counts: StoredCounts, attemptNumber: Int)(implicit ec: ExecutionContext): Future[Unit] = {
74 | if (attemptNumber < 3) {
75 | statisticsRepository.storeCounts(counts).recoverWith {
76 | case NonFatal(t) => retryStoring(counts, attemptNumber + 1)
77 | }
78 | } else {
79 | Future.failed(CountStorageException(counts))
80 | }
81 | }
82 |
83 | }
84 |
85 | class StatisticsServiceFailed(cause: Throwable)
86 | extends RuntimeException(cause) {
87 | def this(message: String) = this(new RuntimeException(message))
88 | def this(message: String, cause: Throwable) =
89 | this(new RuntimeException(message, cause))
90 | }
91 | object StatisticsServiceFailed {
92 | def apply(message: String): StatisticsServiceFailed =
93 | new StatisticsServiceFailed(message)
94 | def apply(message: String, cause: Throwable):
95 | StatisticsServiceFailed =
96 | new StatisticsServiceFailed(message, cause)
97 | }
--------------------------------------------------------------------------------
/CH05/app/services/TwitterService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import play.api.Play
4 | import play.api.Play.current
5 | import play.api.libs.oauth.{OAuthCalculator, RequestToken, ConsumerKey}
6 | import play.api.libs.ws.WS
7 |
8 | import scala.concurrent.{ExecutionContext, Future}
9 |
10 | case class TwitterCounts(followersCount: Long, friendsCount: Long)
11 |
12 | trait TwitterService {
13 |
14 | def fetchRelationshipCounts(userName: String)(implicit ec: ExecutionContext): Future[TwitterCounts]
15 |
16 | def postTweet(message: String)(implicit ec: ExecutionContext): Future[Unit]
17 |
18 | }
19 |
20 | class WSTwitterService extends TwitterService {
21 |
22 | override def fetchRelationshipCounts(userName: String)(implicit ec: ExecutionContext): Future[TwitterCounts] = {
23 |
24 | credentials.map {
25 | case (consumerKey, requestToken) =>
26 | WS.url("https://api.twitter.com/1.1/users/show.json")
27 | .sign(OAuthCalculator(consumerKey, requestToken))
28 | .withQueryString("screen_name" -> userName)
29 | .get().map { response =>
30 | if (response.status == 200) {
31 | TwitterCounts(
32 | (response.json \ "followers_count").as[Long],
33 | (response.json \ "friends_count").as[Long]
34 | )
35 | } else {
36 | throw new TwitterServiceException(s"Could not retrieve counts for Twitter user $userName")
37 | }
38 | }
39 | }.getOrElse {
40 | Future.failed(new TwitterServiceException("You did not correctly configure the Twitter credentials"))
41 | }
42 |
43 | }
44 |
45 | override def postTweet(message: String)(implicit ec: ExecutionContext): Future[Unit] = Future.successful {
46 | println("TWITTER: " + message)
47 | }
48 |
49 | private def credentials = for {
50 | apiKey <- Play.configuration.getString("twitter.apiKey")
51 | apiSecret <- Play.configuration.getString("twitter.apiSecret")
52 | token <- Play.configuration.getString("twitter.accessToken")
53 | tokenSecret <- Play.configuration.getString("twitter.accessTokenSecret")
54 | } yield (ConsumerKey(apiKey, apiSecret), RequestToken(token, tokenSecret))
55 |
56 | }
57 |
58 | case class TwitterServiceException(message: String) extends RuntimeException(message)
--------------------------------------------------------------------------------
/CH05/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(message: String)
2 |
3 | @main("Welcome to Play") {
4 |
5 | @play20.welcome(message)
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/CH05/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)
2 |
3 |
4 |
5 |
6 |
7 | @title
8 |
9 |
10 |
11 |
12 |
13 | @content
14 |
15 |
16 |
--------------------------------------------------------------------------------
/CH05/build.sbt:
--------------------------------------------------------------------------------
1 | name := "CH05"
2 |
3 | version := "1.0"
4 |
5 | lazy val `ch05` = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.7"
8 |
9 | libraryDependencies ++= Seq(
10 | ws,
11 | specs2,
12 | "org.reactivemongo" %% "play2-reactivemongo" % "0.11.0.play24"
13 | )
14 |
15 | unmanagedResourceDirectories in Test <+= baseDirectory ( _ /"target/web/public/test" )
16 |
--------------------------------------------------------------------------------
/CH05/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.crypto.secret = "%APPLICATION_SECRET%"
2 |
3 | play.i18n.langs = ["en"]
4 |
5 | # MongoDB
6 | mongodb.uri = "mongodb://localhost:27017/twitter_statistics"
7 |
8 | include "twitter.conf"
--------------------------------------------------------------------------------
/CH05/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CH05/conf/play.plugins:
--------------------------------------------------------------------------------
1 | 1100:play.modules.reactivemongo.ReactiveMongoPlugin
--------------------------------------------------------------------------------
/CH05/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 |
8 | # Map static resources from the /public folder to the /assets URL path
9 | GET /assets/*file controllers.Assets.at(path="/public", file)
10 |
11 |
--------------------------------------------------------------------------------
/CH05/conf/twitter.conf:
--------------------------------------------------------------------------------
1 | # Twitter
2 | twitter.apiKey=""
3 | twitter.apiSecret=""
4 | twitter.accessToken=""
5 | twitter.accessTokenSecret=""
6 |
--------------------------------------------------------------------------------
/CH05/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/CH05/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
--------------------------------------------------------------------------------
/CH05/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH05/public/images/favicon.png
--------------------------------------------------------------------------------
/CH05/public/stylesheets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH05/public/stylesheets/main.css
--------------------------------------------------------------------------------
/CH05/test/services/AuthenticationServiceSpec.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import org.specs2.concurrent.ExecutionEnv
4 | import org.specs2.mutable.Specification
5 |
6 | import scala.concurrent.duration._
7 |
8 | class AuthenticationServiceSpec extends Specification {
9 |
10 | "The AuthenticationService" should {
11 |
12 | val service: AuthenticationService = new DummyAuthenticationService
13 |
14 | "correctly authenticate Bob Marley" in { implicit ee: ExecutionEnv =>
15 | service.authenticateUser("bob@marley.org", "secret") must beEqualTo (AuthenticationSuccessful).await(1, 200.millis)
16 | }
17 |
18 | "not authenticate Ziggy Marley" in { implicit ee: ExecutionEnv =>
19 | service.authenticateUser("ziggy@marley.org", "secret") must beEqualTo (AuthenticationUnsuccessful).await(1, 200.millis)
20 | }
21 |
22 | "fail if it takes too long" in { implicit ee: ExecutionEnv =>
23 | service.authenticateUser("jimmy@hendrix.com", "secret") must throwA[RuntimeException].await(1, 600.millis)
24 | }
25 |
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/CH05/test/services/StatisticsServiceSpec.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import org.specs2.concurrent.ExecutionEnv
4 | import org.specs2.mutable.Specification
5 | import org.specs2.specification.mutable.ExecutionEnvironment
6 | import play.api.inject.guice.GuiceApplicationBuilder
7 | import play.api.test.WithApplication
8 | import play.modules.reactivemongo._
9 |
10 | import scala.concurrent.duration._
11 |
12 | class StatisticsServiceSpec() extends Specification with ExecutionEnvironment {
13 |
14 | def is(implicit ee: ExecutionEnv) = {
15 | "The StatisticsService" should {
16 |
17 | "compute and publish statistics" in new WithApplication() {
18 | val repository = new MongoStatisticsRepository(configuredAppBuilder.injector.instanceOf[ReactiveMongoApi])
19 | val wsTwitterService = new WSTwitterService
20 | val service = new DefaultStatisticsService(repository, wsTwitterService)
21 |
22 | val f = service.createUserStatistics("elmanu")
23 |
24 | f must beEqualTo(()).await(retries = 0, timeout = 5.seconds)
25 | }
26 |
27 | }
28 |
29 | def configuredAppBuilder = {
30 | import scala.collection.JavaConversions.iterableAsScalaIterable
31 |
32 | val env = play.api.Environment.simple(mode = play.api.Mode.Test)
33 | val config = play.api.Configuration.load(env)
34 | val modules = config.getStringList("play.modules.enabled").fold(
35 | List.empty[String])(l => iterableAsScalaIterable(l).toList)
36 |
37 | new GuiceApplicationBuilder().
38 | configure("play.modules.enabled" -> (modules :+
39 | "play.modules.reactivemongo.ReactiveMongoModule")).build
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/CH06/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 | /conf/twitter.conf
--------------------------------------------------------------------------------
/CH06/app/actors/StatisticsProvider.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.SupervisorStrategy.{Escalate, Restart}
4 | import akka.actor._
5 | import messages.ComputeReach
6 | import org.joda.time.{Interval, DateTime}
7 | import reactivemongo.core.errors.ConnectionException
8 | import scala.concurrent.duration._
9 | import StatisticsProvider._
10 |
11 | class StatisticsProvider extends Actor with ActorLogging {
12 |
13 | var reachComputer: ActorRef = _
14 | var storage: ActorRef = _
15 | var followersCounter: ActorRef = _
16 |
17 | implicit val ec = context.dispatcher
18 |
19 | override def preStart(): Unit = {
20 | log.info("Starting StatisticsProvider")
21 | followersCounter = context.actorOf(Props[UserFollowersCounter], name = "userFollowersCounter")
22 | storage = context.actorOf(Props[Storage], name = "storage")
23 | reachComputer = context.actorOf(TweetReachComputer.props(followersCounter, storage), name = "tweetReachComputer")
24 |
25 | context.watch(storage)
26 | }
27 |
28 | override def supervisorStrategy: SupervisorStrategy =
29 | OneForOneStrategy(maxNrOfRetries = 3, withinTimeRange = 2.minutes) {
30 | case _: ConnectionException =>
31 | Restart
32 | case t: Throwable =>
33 | super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate)
34 | }
35 |
36 | def receive = {
37 | case reach: ComputeReach =>
38 | log.info("Forwarding ComputeReach message to the reach computer")
39 | reachComputer forward reach
40 | case Terminated(terminatedStorageRef) =>
41 | context.system.scheduler.scheduleOnce(1.minute, self, ReviveStorage)
42 | context.become(storageUnavailable)
43 | case TwitterRateLimitReached(reset) =>
44 | context.system.scheduler.scheduleOnce(
45 | new Interval(DateTime.now, reset).toDurationMillis.millis,
46 | self,
47 | ResumeService
48 | )
49 | context.become(serviceUnavailable)
50 | case UserFollowersCounterUnavailable =>
51 | context.become(followersCountUnavailable)
52 | }
53 |
54 | def storageUnavailable: Receive = {
55 | case reach @ ComputeReach(_) =>
56 | sender() ! ServiceUnavailable
57 | case ReviveStorage =>
58 | storage = context.actorOf(Props[Storage], name = "storage")
59 | context.unbecome()
60 | }
61 |
62 | def serviceUnavailable: Receive = {
63 | case reach: ComputeReach =>
64 | sender() ! ServiceUnavailable
65 | case ResumeService =>
66 | context.unbecome()
67 | }
68 |
69 | def followersCountUnavailable: Receive = {
70 | case UserFollowersCounterAvailable =>
71 | context.unbecome()
72 | case reach: ComputeReach =>
73 | sender() ! ServiceUnavailable
74 | }
75 |
76 | }
77 |
78 | object StatisticsProvider {
79 | def props = Props[StatisticsProvider]
80 |
81 | case object ServiceUnavailable
82 | case object ReviveStorage
83 | case object ResumeService
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/CH06/app/actors/Storage.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import actors.Storage._
4 | import akka.actor.{Actor, ActorLogging, ActorRef}
5 | import akka.pattern.pipe
6 | import messages.{ReachStored, StoreReach}
7 | import org.joda.time.DateTime
8 | import reactivemongo.api.collections.bson.BSONCollection
9 | import reactivemongo.api.commands.WriteResult
10 | import reactivemongo.api.{DefaultDB, MongoConnection, MongoDriver}
11 | import reactivemongo.bson._
12 | import reactivemongo.core.errors.ConnectionException
13 |
14 | case class StoredReach(when: DateTime, tweetId: BigInt, score: Int)
15 |
16 | object StoredReach {
17 |
18 | implicit object BigIntHandler extends BSONDocumentReader[BigInt] with BSONDocumentWriter[BigInt] {
19 | def write(bigInt: BigInt): BSONDocument = BSONDocument(
20 | "signum" -> bigInt.signum,
21 | "value" -> BSONBinary(bigInt.toByteArray, Subtype.UserDefinedSubtype))
22 |
23 | def read(doc: BSONDocument): BigInt = BigInt(
24 | doc.getAs[Int]("signum").get, {
25 | val buf = doc.getAs[BSONBinary]("value").get.value
26 | buf.readArray(buf.readable())
27 | })
28 | }
29 |
30 | implicit object StoredReachHandler extends BSONDocumentReader[StoredReach] with BSONDocumentWriter[StoredReach] {
31 | override def read(bson: BSONDocument): StoredReach = {
32 | val when = bson.getAs[BSONDateTime]("when").map(t => new DateTime(t.value)).get
33 | val tweetId = bson.getAs[BigInt]("tweet_id").get
34 | val score = bson.getAs[Int]("score").get
35 | StoredReach(when, tweetId, score)
36 | }
37 |
38 | override def write(r: StoredReach): BSONDocument = BSONDocument(
39 | "when" -> BSONDateTime(r.when.getMillis),
40 | "tweetId" -> r.tweetId,
41 | "tweet_id" -> r.tweetId,
42 | "score" -> r.score
43 | )
44 | }
45 |
46 | }
47 |
48 | class Storage() extends Actor with ActorLogging {
49 |
50 | val Database = "twitterService"
51 | val ReachCollection = "ComputedReach"
52 |
53 | implicit val executionContext = context.dispatcher
54 |
55 | val driver: MongoDriver = new MongoDriver
56 | var connection: MongoConnection = _
57 | var db: DefaultDB = _
58 | var collection: BSONCollection = _
59 | obtainConnection()
60 |
61 | override def postRestart(reason: Throwable): Unit = {
62 | reason match {
63 | case ce: ConnectionException =>
64 | // try to obtain a brand new connection
65 | obtainConnection()
66 | }
67 | super.postRestart(reason)
68 | }
69 |
70 | override def postStop(): Unit = {
71 | connection.close()
72 | driver.close()
73 | }
74 |
75 | var currentWrites = Set.empty[BigInt]
76 |
77 | def receive = {
78 | case StoreReach(tweetId, score) =>
79 | log.info("Storing reach for tweet {}", tweetId)
80 | if (!currentWrites.contains(tweetId)) {
81 | currentWrites = currentWrites + tweetId
82 | val originalSender = sender()
83 | collection.insert(StoredReach(DateTime.now, tweetId, score)).map { lastError =>
84 | LastStorageError(lastError, tweetId, originalSender)
85 | }.recover {
86 | case _ =>
87 | currentWrites = currentWrites - tweetId
88 | } pipeTo self
89 | }
90 | case LastStorageError(error, tweetId, client) =>
91 | if(error.inError) {
92 | currentWrites = currentWrites - tweetId
93 | } else {
94 | client ! ReachStored(tweetId)
95 | }
96 | }
97 |
98 | private def obtainConnection(): Unit = {
99 | connection = driver.connection(List("localhost"))
100 | db = connection.db(Database)
101 | collection = db.collection[BSONCollection](ReachCollection)
102 | }
103 | }
104 |
105 | object Storage {
106 | case class LastStorageError(result: WriteResult, tweetId: BigInt, client: ActorRef)
107 | }
--------------------------------------------------------------------------------
/CH06/app/actors/TwitterCredentials.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import play.api.Play
4 | import play.api.libs.oauth.{RequestToken, ConsumerKey}
5 |
6 | trait TwitterCredentials {
7 |
8 | import play.api.Play.current
9 |
10 | protected def credentials = for {
11 | apiKey <- Play.configuration.getString("twitter.apiKey")
12 | apiSecret <- Play.configuration.getString("twitter.apiSecret")
13 | token <- Play.configuration.getString("twitter.accessToken")
14 | tokenSecret <- Play.configuration.getString("twitter.accessTokenSecret")
15 | } yield (ConsumerKey(apiKey, apiSecret), RequestToken(token, tokenSecret))
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/CH06/app/actors/UserFollowersCounter.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.{ActorLogging, Actor}
4 | import messages.{FollowerCount, FetchFollowerCount}
5 | import org.joda.time.DateTime
6 | import play.api.libs.oauth.OAuthCalculator
7 | import play.api.libs.ws.WS
8 | import play.api.Play.current
9 |
10 | import akka.dispatch.ControlMessage
11 | import akka.pattern.{CircuitBreaker, pipe}
12 |
13 | import scala.concurrent.Future
14 |
15 | import scala.concurrent.duration._
16 |
17 | class UserFollowersCounter extends Actor with ActorLogging with TwitterCredentials {
18 |
19 | implicit val ec = context.dispatcher
20 |
21 | val breaker =
22 | new CircuitBreaker(context.system.scheduler,
23 | maxFailures = 5,
24 | callTimeout = 2.seconds,
25 | resetTimeout = 1.minute
26 | )
27 |
28 | def receive = {
29 | case FetchFollowerCount(tweetId, user) =>
30 | val originalSender = sender()
31 | breaker.onOpen({
32 | log.info("Circuit breaker open")
33 | originalSender ! FollowerCountUnavailable(tweetId, user)
34 | context.parent ! UserFollowersCounterUnavailable
35 | }).onHalfOpen(
36 | log.info("Circuit breaker half-open")
37 | ).onClose({
38 | log.info("Circuit breaker closed")
39 | context.parent ! UserFollowersCounterAvailable
40 | }).withCircuitBreaker(fetchFollowerCount(tweetId, user)) pipeTo sender()
41 | }
42 |
43 |
44 | val LimitRemaining = "X-Rate-Limit-Remaining"
45 | val LimitReset = "X-Rate-Limit-Reset"
46 |
47 | private def fetchFollowerCount(tweetId: BigInt, userId: BigInt): Future[FollowerCount] = {
48 | credentials.map {
49 | case (consumerKey, requestToken) =>
50 | WS.url("https://api.twitter.com/1.1/users/show.json")
51 | .sign(OAuthCalculator(consumerKey, requestToken))
52 | .withQueryString("user_id" -> userId.toString)
53 | .get().map { response =>
54 | if (response.status == 200) {
55 |
56 | val rateLimit = for {
57 | remaining <- response.header(LimitRemaining)
58 | reset <- response.header(LimitReset)
59 | } yield {
60 | (remaining.toInt, new DateTime(reset.toLong * 1000))
61 | }
62 |
63 | rateLimit.foreach { case (remaining, reset) =>
64 | log.info("Rate limit: {} requests remaining, window resets at {}", remaining, reset)
65 | if (remaining < 50) {
66 | Thread.sleep(10000)
67 | }
68 | if (remaining < 10) {
69 | context.parent ! TwitterRateLimitReached(reset)
70 | }
71 | }
72 |
73 | val count = (response.json \ "followers_count").as[Int]
74 | FollowerCount(tweetId, userId, count)
75 | } else {
76 | throw new RuntimeException(s"Could not retrieve followers count for user $userId")
77 | }
78 | }
79 | }.getOrElse {
80 | Future.failed(new RuntimeException("You did not correctly configure the Twitter credentials"))
81 | }
82 | }
83 |
84 | }
85 |
86 | case class TwitterRateLimitReached(reset: DateTime) extends ControlMessage
87 | case class FollowerCountUnavailable(tweetId: BigInt, user: BigInt)
88 | case object UserFollowersCounterUnavailable extends ControlMessage
89 | case object UserFollowersCounterAvailable extends ControlMessage
--------------------------------------------------------------------------------
/CH06/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject._
4 |
5 | import actors.StatisticsProvider
6 | import akka.actor.ActorSystem
7 | import akka.util.Timeout
8 | import messages.{TweetReachCouldNotBeComputed, TweetReach, ComputeReach}
9 | import play.api.libs.concurrent.Execution.Implicits._
10 | import play.api.mvc._
11 |
12 | import akka.pattern.ask
13 | import scala.concurrent.duration._
14 |
15 | class Application @Inject() (system: ActorSystem) extends Controller {
16 |
17 | lazy val statisticsProvider = system.actorSelection("akka://application/user/statisticsProvider")
18 |
19 | def computeReach(tweetId: String) = Action.async {
20 | implicit val timeout = Timeout(5.minutes)
21 | val eventuallyReach = statisticsProvider ? ComputeReach(BigInt(tweetId))
22 | eventuallyReach.map {
23 | case tr: TweetReach =>
24 | Ok(tr.score.toString)
25 | case StatisticsProvider.ServiceUnavailable =>
26 | ServiceUnavailable("Sorry")
27 | case TweetReachCouldNotBeComputed =>
28 | ServiceUnavailable("Sorry")
29 | }
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/CH06/app/messages/Messages.scala:
--------------------------------------------------------------------------------
1 | package messages
2 |
3 | case class ComputeReach(tweetId: BigInt)
4 | case class TweetReach(tweetId: BigInt, score: Int)
5 | case object TweetReachCouldNotBeComputed
6 |
7 | case class FetchFollowerCount(tweetId: BigInt, userId: BigInt)
8 | case class FollowerCount(tweetId: BigInt, userId: BigInt, followersCount: Int)
9 |
10 | case class StoreReach(tweetId: BigInt, score: Int)
11 | case class ReachStored(tweetId: BigInt)
--------------------------------------------------------------------------------
/CH06/app/modules/Actors.scala:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import javax.inject._
4 |
5 | import actors.StatisticsProvider
6 | import akka.actor.ActorSystem
7 | import com.google.inject.AbstractModule
8 |
9 | class Actors @Inject()(system: ActorSystem) extends ApplicationActors {
10 | system.actorOf(
11 | props = StatisticsProvider.props.withDispatcher("control-aware-dispatcher"),
12 | name = "statisticsProvider"
13 | )
14 | }
15 |
16 | trait ApplicationActors
17 |
18 | class ActorsModule extends AbstractModule {
19 | override def configure(): Unit = {
20 | bind(classOf[ApplicationActors]).to(classOf[Actors]).asEagerSingleton
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/CH06/build.sbt:
--------------------------------------------------------------------------------
1 | name := "CH06"
2 |
3 | version := "1.0"
4 |
5 | lazy val `ch06` = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.7"
8 |
9 | libraryDependencies ++= Seq(
10 | ws,
11 | "org.reactivemongo" %% "play2-reactivemongo" % "0.11.7.play24",
12 | "com.typesafe.akka" %% "akka-actor" % "2.4.0",
13 | "com.typesafe.akka" %% "akka-slf4j" % "2.4.0"
14 |
15 | )
16 |
17 | routesGenerator := InjectedRoutesGenerator
18 |
19 | libraryDependencies += "com.ning" % "async-http-client" % "1.9.29"
20 |
--------------------------------------------------------------------------------
/CH06/conf/application.conf:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for the application.
2 | # ~~~~~
3 |
4 | # Secret key
5 | # ~~~~~
6 | # The secret key is used to secure cryptographics functions.
7 | # If you deploy your application to several instances be sure to use the same key!
8 | play.crypto.secret="to/]]xGbIWiirwgKp3;jVMoK]b>C3ePBO5uGddNQSLSgh:RAI2ip?hzJiF7Gt/em"
9 |
10 | # The application languages
11 | # ~~~~~
12 | play.i18n.langs = ["en"]
13 |
14 | # Global object class
15 | # ~~~~~
16 | # Define the Global object class for this application.
17 | # Default to Global in the root package.
18 | # application.global=Global
19 |
20 | # Router
21 | # ~~~~~
22 | # Define the Router object to use for this application.
23 | # This router will be looked up first when the application is starting up,
24 | # so make sure this is the entry point.
25 | # Furthermore, it's assumed your route file is named properly.
26 | # So for an application router like `my.application.Router`,
27 | # you may need to define a router file `conf/my.application.routes`.
28 | # Default to Routes in the root package (and conf/routes)
29 | # application.router=my.application.Routes
30 |
31 | # Database configuration
32 | # ~~~~~
33 | # You can declare as many datasources as you want.
34 | # By convention, the default datasource is named `default`
35 | #
36 | # db.default.driver=org.h2.Driver
37 | # db.default.url="jdbc:h2:mem:play"
38 | # db.default.user=sa
39 | # db.default.password=""
40 |
41 | # Evolutions
42 | # ~~~~~
43 | # You can disable evolutions if needed
44 | # evolutionplugin=disabled
45 |
46 | play.modules.enabled += "modules.ActorsModule"
47 |
48 | akka {
49 | loggers = ["akka.event.slf4j.Slf4jLogger"]
50 | loglevel = "DEBUG"
51 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
52 | }
53 |
54 | control-aware-dispatcher {
55 | mailbox-type = "akka.dispatch.UnboundedControlAwareMailbox"
56 | }
57 |
58 | include "twitter.conf"
59 |
--------------------------------------------------------------------------------
/CH06/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/CH06/conf/routes:
--------------------------------------------------------------------------------
1 | GET /:tweetId controllers.Application.computeReach(tweetId)
2 |
3 | # Map static resources from the /public folder to the /assets URL path
4 | GET /assets/*file controllers.Assets.at(path="/public", file)
--------------------------------------------------------------------------------
/CH06/conf/twitter.conf:
--------------------------------------------------------------------------------
1 | # Twitter
2 | twitter.apiKey=""
3 | twitter.apiSecret=""
4 | twitter.accessToken=""
5 | twitter.accessTokenSecret=""
6 |
--------------------------------------------------------------------------------
/CH06/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/CH06/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
--------------------------------------------------------------------------------
/CH06/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH06/public/images/favicon.png
--------------------------------------------------------------------------------
/CH06/public/stylesheets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH06/public/stylesheets/main.css
--------------------------------------------------------------------------------
/CH07/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 | /conf/twitter.conf
--------------------------------------------------------------------------------
/CH07/README.md:
--------------------------------------------------------------------------------
1 | = Chapter 7
2 |
3 | == Configuration
4 |
5 | - make sure to install postgreSQL and to create the `chapter7` database with the correct credentials
6 | - put your twitter credentials in `conf/twitter.conf`
7 |
8 | == Usage
9 |
10 | - login via http://localhost:9000/login (user: bob@marley.org, password: secret)
11 | - connect via telnet: `telnet localhost 6666`
12 | - register: `+123 register twitterHandle`
13 | - subscribe mentions: `+123 subscribe mentions`
--------------------------------------------------------------------------------
/CH07/app/actors/CQRSCommandHandler.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.{Props, ActorLogging}
4 | import akka.persistence.{RecoveryFailure, RecoveryCompleted, PersistentActor}
5 |
6 | class CQRSCommandHandler extends PersistentActor with ActorLogging {
7 |
8 | override def persistenceId: String = "CQRSCommandHandler"
9 |
10 | override def receiveRecover: Receive = {
11 | case RecoveryFailure(cause) => log.error(cause, "Failed to recover!")
12 | case RecoveryCompleted => log.info("Recovery completed")
13 | case evt: Event => handleEvent(evt)
14 | }
15 |
16 | override def receiveCommand: Receive = {
17 | case RegisterUser(phoneNumber, username) =>
18 | persist(UserRegistered(phoneNumber, username))(handleEvent)
19 | case command: Command =>
20 | context.child(command.phoneNumber).map { reference =>
21 | log.info("Forwarding message {} to {}", command, reference)
22 | reference forward command
23 | } getOrElse {
24 | sender() ! UnknownUser(command.phoneNumber)
25 | }
26 | }
27 |
28 | def handleEvent(event: Event): Unit = event match {
29 | case registered @ UserRegistered(phoneNumber, userName, _) =>
30 | context.actorOf(
31 | props = Props(classOf[ClientCommandHandler], phoneNumber, userName),
32 | name = phoneNumber
33 | )
34 | if (recoveryFinished) {
35 | sender() ! registered
36 | context.system.eventStream.publish(registered)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/CH07/app/actors/CQRSEventHandler.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import java.sql.Timestamp
4 |
5 | import akka.actor.{Props, Actor, ActorLogging}
6 | import helpers.Database
7 | import generated.Tables._
8 | import org.jooq.impl.DSL._
9 |
10 | class CQRSEventHandler(database: Database) extends Actor with ActorLogging {
11 |
12 | override def preStart(): Unit = {
13 | context.system.eventStream.subscribe(self, classOf[Event])
14 | }
15 |
16 | def receive = {
17 | case UserRegistered(phoneNumber, userName, timestamp) =>
18 | database.withTransaction { sql =>
19 | sql.insertInto(TWITTER_USER)
20 | .columns(TWITTER_USER.CREATED_ON, TWITTER_USER.PHONE_NUMBER, TWITTER_USER.TWITTER_USER_NAME)
21 | .values(new Timestamp(timestamp.getMillis), phoneNumber, userName)
22 | .execute()
23 | }
24 | case ClientEvent(phoneNumber, userName, MentionsSubscribed(timestamp), _) =>
25 | database.withTransaction { sql =>
26 | sql.insertInto(MENTION_SUBSCRIPTIONS)
27 | .columns(MENTION_SUBSCRIPTIONS.USER_ID, MENTION_SUBSCRIPTIONS.CREATED_ON)
28 | .select(
29 | select(TWITTER_USER.ID, value(new Timestamp(timestamp.getMillis)))
30 | .from(TWITTER_USER)
31 | .where(
32 | TWITTER_USER.PHONE_NUMBER.equal(phoneNumber)
33 | .and(
34 | TWITTER_USER.TWITTER_USER_NAME.equal(userName)
35 | )
36 | )
37 | ).execute()
38 | }
39 | case ClientEvent(phoneNumber, userName, MentionReceived(id, created_on, from, text, timestamp), _) =>
40 | database.withTransaction { sql =>
41 | sql.insertInto(MENTIONS)
42 | .columns(
43 | MENTIONS.USER_ID,
44 | MENTIONS.CREATED_ON,
45 | MENTIONS.TWEET_ID,
46 | MENTIONS.AUTHOR_USER_NAME,
47 | MENTIONS.TEXT
48 | )
49 | .select(
50 | select(
51 | TWITTER_USER.ID,
52 | value(new Timestamp(timestamp.getMillis)),
53 | value(id),
54 | value(from),
55 | value(text)
56 | )
57 | .from(TWITTER_USER)
58 | .where(
59 | TWITTER_USER.PHONE_NUMBER.equal(phoneNumber)
60 | .and(
61 | TWITTER_USER.TWITTER_USER_NAME.equal(userName)
62 | )
63 | )
64 | ).execute()
65 | }
66 | }
67 |
68 | }
69 |
70 | object CQRSEventHandler {
71 | def props(database: Database) = Props(classOf[CQRSEventHandler], database)
72 | }
--------------------------------------------------------------------------------
/CH07/app/actors/CQRSQueryHandler.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.{Props, Actor}
4 | import helpers.Database
5 | import generated.Tables._
6 | import org.jooq.impl.DSL._
7 | import org.jooq.util.postgres.PostgresDataType
8 | import akka.pattern.pipe
9 |
10 | import scala.concurrent.Future
11 | import scala.util.control.NonFatal
12 |
13 | class CQRSQueryHandler(database: Database) extends Actor {
14 |
15 | implicit val ec = context.dispatcher
16 |
17 | override def receive = {
18 | case MentionsToday(phoneNumber) =>
19 | countMentions(phoneNumber).map { count =>
20 | DailyMentionsCount(count)
21 | } recover { case NonFatal(t) =>
22 | QueryFailed
23 | } pipeTo sender()
24 | }
25 |
26 | def countMentions(phoneNumber: String): Future[Int] =
27 | database.query { sql =>
28 | sql.selectCount().from(MENTIONS).where(
29 | MENTIONS.CREATED_ON.greaterOrEqual(currentDate().cast(PostgresDataType.TIMESTAMP))
30 | .and(MENTIONS.USER_ID.equal(
31 | sql.select(TWITTER_USER.ID)
32 | .from(TWITTER_USER)
33 | .where(TWITTER_USER.PHONE_NUMBER.equal(phoneNumber)))
34 | )
35 | ).fetchOne().value1()
36 | }
37 | }
38 | object CQRSQueryHandler {
39 | def props(database: Database) = Props(classOf[CQRSQueryHandler], database)
40 | }
--------------------------------------------------------------------------------
/CH07/app/actors/Messages.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import org.joda.time.DateTime
4 |
5 | trait Command {
6 | val phoneNumber: String
7 | }
8 |
9 | trait Event {
10 | val timestamp: DateTime
11 | }
12 |
13 | trait Query
14 |
15 | case class MentionsToday(phoneNumber: String) extends Query
16 | case class MentionsPastWeek(phoneNumber: String) extends Query
17 |
18 | trait QueryResult
19 | case class DailyMentionsCount(count: Int) extends QueryResult
20 | case class WeeklyMentionsCount(count: Int) extends QueryResult
21 | case object QueryFailed extends QueryResult
22 |
23 | case class RegisterUser(phoneNumber: String, userName: String) extends Command
24 | case class ConnectUser(phoneNumber: String) extends Command
25 | case class SubscribeMentions(phoneNumber: String) extends Command
26 |
27 | case class AcknowledgeMention(id: String) // this message is sent back directly without phone number, so we don't extend our Command here
28 |
29 | case class UserRegistered(phoneNumber: String, userName: String, timestamp: DateTime = DateTime.now) extends Event
30 | case class MentionsSubscribed(timestamp: DateTime = DateTime.now) extends Event
31 | case class MentionReceived(id: String, created_on: DateTime, from: String, text: String, timestamp: DateTime = DateTime.now) extends Event
32 | case class MentionAcknowledged(id: String, timestamp: DateTime = DateTime.now) extends Event
33 |
34 | case class ClientEvent(phoneNumber: String, userName: String, event: Event, timestamp: DateTime = DateTime.now) extends Event
35 |
36 | case class UnknownUser(phoneNumber: String)
37 |
38 | case class InvalidCommand(reason: String)
--------------------------------------------------------------------------------
/CH07/app/actors/SMSHandler.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.{ActorRef, ActorLogging, Actor}
4 | import akka.io.Tcp._
5 | import akka.util.{ByteString, Timeout}
6 | import scala.concurrent.duration._
7 |
8 | class SMSHandler(connection: ActorRef) extends Actor with ActorLogging {
9 |
10 | implicit val timeout = Timeout(2.seconds)
11 |
12 | implicit val ec = context.dispatcher
13 |
14 | lazy val commandHandler = context.actorSelection(
15 | "akka://application/user/sms/commandHandler"
16 | )
17 | lazy val queryHandler = context.actorSelection(
18 | "akka://application/user/sms/queryHandler"
19 | )
20 |
21 | val MessagePattern = """[\+]([0-9]*) (.*)""".r
22 | val RegistrationPattern = """register (.*)""".r
23 |
24 | def receive = {
25 | case Received(data) =>
26 | log.info("Received message: {}", data.utf8String)
27 | data.utf8String.trim match {
28 | case MessagePattern(number, message) =>
29 | handleMessage(number, message)
30 | case other =>
31 | log.warning("Invalid message {}", other)
32 | sender() ! Write(ByteString("Invalid message format\n"))
33 | }
34 | case registered: UserRegistered =>
35 | connection ! Write(ByteString("Registration successful\n"))
36 | case subscribed: MentionsSubscribed =>
37 | connection ! Write(ByteString("Mentions subscribed\n"))
38 | case UnknownUser(number) =>
39 | connection ! Write(ByteString(s"Unknown user $number\n"))
40 | case InvalidCommand(reason) =>
41 | connection ! Write(ByteString(reason + "\n"))
42 | case MentionReceived(id, created, from, text, _) =>
43 | connection ! Write(ByteString(s"mentioned by @$from: $text\n"))
44 | sender() ! AcknowledgeMention(id)
45 | case DailyMentionsCount(count) =>
46 | connection ! Write(ByteString(s"$count mentions today\n"))
47 | case WeeklyMentionsCount(count) =>
48 | connection ! Write(ByteString(s"$count mentions this week\n"))
49 | case QueryFailed =>
50 | connection ! Write(ByteString("Sorry, we couldn't run your query\n"))
51 | case PeerClosed =>
52 | context stop self
53 | }
54 |
55 | def handleMessage(number: String, message: String) = {
56 | message match {
57 | case RegistrationPattern(userName) =>
58 | commandHandler ! RegisterUser(number, userName)
59 | case "subscribe mentions" =>
60 | commandHandler ! SubscribeMentions(number)
61 | case "connect" =>
62 | commandHandler ! ConnectUser(number)
63 | case "mentions today" =>
64 | queryHandler ! MentionsToday(number)
65 | case "mentions past week" =>
66 | queryHandler ! MentionsPastWeek(number)
67 | case other =>
68 | log.warning("Invalid message {}", other)
69 | }
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/CH07/app/actors/SMSServer.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import java.net.InetSocketAddress
4 |
5 | import akka.actor.{Props, ActorLogging, Actor}
6 | import akka.io.{Tcp, IO}
7 | import akka.io.Tcp._
8 |
9 | class SMSServer extends Actor with ActorLogging {
10 |
11 | import context.system
12 |
13 | IO(Tcp) ! Bind(self, new InetSocketAddress("localhost", 6666))
14 |
15 | def receive = {
16 | case Bound(localAddress) =>
17 | log.info("SMS server listening on {}", localAddress)
18 |
19 | case CommandFailed(_: Bind) =>
20 | context stop self
21 |
22 | case Connected(remote, local) =>
23 | val connection = sender()
24 | val handler = context.actorOf(Props(classOf[SMSHandler], connection))
25 | connection ! Register(handler)
26 | }
27 | }
--------------------------------------------------------------------------------
/CH07/app/actors/SMSService.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import javax.inject.Inject
4 |
5 | import akka.actor.{ActorLogging, Actor, Props}
6 | import com.google.inject.AbstractModule
7 | import helpers.Database
8 | import play.api.libs.concurrent.AkkaGuiceSupport
9 |
10 | class SMSService @Inject() (database: Database) extends Actor with ActorLogging {
11 |
12 | override def preStart(): Unit = {
13 | context.actorOf(Props[SMSServer])
14 | context.actorOf(Props[CQRSCommandHandler], name = "commandHandler")
15 | context.actorOf(CQRSQueryHandler.props(database), name = "queryHandler")
16 | context.actorOf(CQRSEventHandler.props(database), name = "eventHandler")
17 | }
18 |
19 | def receive = {
20 | case message =>
21 | log.info("Received {}", message)
22 | }
23 | }
24 |
25 | class SMSServiceModule extends AbstractModule with AkkaGuiceSupport {
26 | def configure(): Unit =
27 | bindActor[SMSService]("sms")
28 | }
--------------------------------------------------------------------------------
/CH07/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 |
5 | import generated.Tables._
6 | import generated.tables.records._
7 | import helpers.Database
8 | import org.jooq.SQLDialect
9 | import org.jooq.impl.DSL
10 | import play.api.Play.current
11 | import play.api.cache.Cache
12 | import play.api.data.Forms._
13 | import play.api.data._
14 | import play.api.db._
15 | import play.api.i18n.{I18nSupport, MessagesApi}
16 | import play.api.libs.Crypto
17 | import play.api.mvc._
18 |
19 | import scala.concurrent.Future
20 |
21 |
22 | class Application @Inject() (
23 | val crypto: Crypto,
24 | val db: Database,
25 | val messagesApi: MessagesApi
26 | ) extends Controller with I18nSupport {
27 |
28 | def index = Authenticated { request =>
29 | Ok(views.html.index(request.user.getFirstname))
30 | }
31 |
32 | def login = Action { implicit request =>
33 | Ok(views.html.login(loginForm))
34 | }
35 |
36 | def logout = Action { implicit request =>
37 | Redirect(routes.Application.index()).withNewSession
38 | }
39 |
40 | def authenticate = Action.async { implicit request =>
41 | loginForm.bindFromRequest.fold(
42 | formWithErrors =>
43 | Future.successful {
44 | BadRequest(views.html.login(formWithErrors))
45 | },
46 | login =>
47 | db.query { sql =>
48 | val user = Option(sql
49 | .selectFrom(USER)
50 | .where(USER.EMAIL.equal(login._1))
51 | .and(USER.PASSWORD.equal(crypto.sign(login._2)))
52 | .fetchOne())
53 |
54 | user.map { u =>
55 | Redirect(routes.Application.index()).withSession(
56 | USER.ID.getName -> u.getId.toString
57 | )
58 | } getOrElse {
59 | BadRequest(
60 | views.html.login(loginForm.withGlobalError("Wrong username or password"))
61 | )
62 | }
63 | }
64 | )
65 | }
66 |
67 | val loginForm = Form(
68 | tuple(
69 | "email" -> email,
70 | "password" -> text
71 | )
72 | )
73 |
74 | }
75 |
76 | case class AuthenticatedRequest[A](userId: Long, user: UserRecord)
77 |
78 | object Authenticated extends ActionBuilder[AuthenticatedRequest] with Results {
79 |
80 | override def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[Result]): Future[Result] = {
81 | val authenticated = for {
82 | id <- request.session.get(USER.ID.getName)
83 | user <- fetchUser(id.toLong)
84 | } yield {
85 | AuthenticatedRequest[A](id.toLong, user)
86 | }
87 |
88 | authenticated.map { authenticatedRequest =>
89 | block(authenticatedRequest)
90 | } getOrElse {
91 | Future.successful {
92 | Redirect(routes.Application.login()).withNewSession
93 | }
94 | }
95 | }
96 |
97 | def fetchUser(id: Long): Option[UserRecord] =
98 | Cache.getAs[UserRecord](id.toString).map { user =>
99 | Some(user)
100 | } getOrElse {
101 | DB.withConnection { connection =>
102 | val sql = DSL.using(connection, SQLDialect.POSTGRES_9_4)
103 | val user = Option(sql.selectFrom[UserRecord](USER).where(USER.ID.equal(id)).fetchOne())
104 | user.foreach { u =>
105 | Cache.set(u.getId.toString, u)
106 | }
107 | user
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/CH07/app/generated/Keys.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 | import generated.tables.records.MentionSubscriptionsRecord
13 | import generated.tables.records.MentionsRecord
14 | import generated.tables.records.PlayEvolutionsRecord
15 | import generated.tables.records.TwitterUserRecord
16 | import generated.tables.records.UserRecord
17 |
18 | import java.lang.Long
19 |
20 | import javax.annotation.Generated
21 |
22 | import org.jooq.Identity
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.AbstractKeys
25 |
26 |
27 | /**
28 | * A class modelling foreign key relationships between tables of the public
29 | * schema
30 | */
31 | @Generated(
32 | value = Array(
33 | "http://www.jooq.org",
34 | "jOOQ version:3.7.0"
35 | ),
36 | comments = "This class is generated by jOOQ"
37 | )
38 | object Keys {
39 |
40 | // -------------------------------------------------------------------------
41 | // IDENTITY definitions
42 | // -------------------------------------------------------------------------
43 |
44 | val IDENTITY_MENTION_SUBSCRIPTIONS = Identities0.IDENTITY_MENTION_SUBSCRIPTIONS
45 | val IDENTITY_MENTIONS = Identities0.IDENTITY_MENTIONS
46 | val IDENTITY_TWITTER_USER = Identities0.IDENTITY_TWITTER_USER
47 | val IDENTITY_USER = Identities0.IDENTITY_USER
48 |
49 | // -------------------------------------------------------------------------
50 | // UNIQUE and PRIMARY KEY definitions
51 | // -------------------------------------------------------------------------
52 |
53 | val MENTION_SUBSCRIPTIONS_PKEY = UniqueKeys0.MENTION_SUBSCRIPTIONS_PKEY
54 | val MENTIONS_PKEY = UniqueKeys0.MENTIONS_PKEY
55 | val PLAY_EVOLUTIONS_PKEY = UniqueKeys0.PLAY_EVOLUTIONS_PKEY
56 | val TWITTER_USER_PKEY = UniqueKeys0.TWITTER_USER_PKEY
57 | val USER_PKEY = UniqueKeys0.USER_PKEY
58 |
59 | // -------------------------------------------------------------------------
60 | // FOREIGN KEY definitions
61 | // -------------------------------------------------------------------------
62 |
63 |
64 | // -------------------------------------------------------------------------
65 | // [#1459] distribute members to avoid static initialisers > 64kb
66 | // -------------------------------------------------------------------------
67 |
68 | private object Identities0 extends AbstractKeys {
69 | val IDENTITY_MENTION_SUBSCRIPTIONS : Identity[MentionSubscriptionsRecord, Long] = AbstractKeys.createIdentity(MentionSubscriptions.MENTION_SUBSCRIPTIONS, MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID)
70 | val IDENTITY_MENTIONS : Identity[MentionsRecord, Long] = AbstractKeys.createIdentity(Mentions.MENTIONS, Mentions.MENTIONS.ID)
71 | val IDENTITY_TWITTER_USER : Identity[TwitterUserRecord, Long] = AbstractKeys.createIdentity(TwitterUser.TWITTER_USER, TwitterUser.TWITTER_USER.ID)
72 | val IDENTITY_USER : Identity[UserRecord, Long] = AbstractKeys.createIdentity(User.USER, User.USER.ID)
73 | }
74 |
75 | private object UniqueKeys0 extends AbstractKeys {
76 | val MENTION_SUBSCRIPTIONS_PKEY : UniqueKey[MentionSubscriptionsRecord] = AbstractKeys.createUniqueKey(MentionSubscriptions.MENTION_SUBSCRIPTIONS, MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID)
77 | val MENTIONS_PKEY : UniqueKey[MentionsRecord] = AbstractKeys.createUniqueKey(Mentions.MENTIONS, Mentions.MENTIONS.ID)
78 | val PLAY_EVOLUTIONS_PKEY : UniqueKey[PlayEvolutionsRecord] = AbstractKeys.createUniqueKey(PlayEvolutions.PLAY_EVOLUTIONS, PlayEvolutions.PLAY_EVOLUTIONS.ID)
79 | val TWITTER_USER_PKEY : UniqueKey[TwitterUserRecord] = AbstractKeys.createUniqueKey(TwitterUser.TWITTER_USER, TwitterUser.TWITTER_USER.ID)
80 | val USER_PKEY : UniqueKey[UserRecord] = AbstractKeys.createUniqueKey(User.USER, User.USER.ID)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/CH07/app/generated/Public.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 |
13 | import java.util.ArrayList
14 | import java.util.Arrays
15 | import java.util.List
16 |
17 | import javax.annotation.Generated
18 |
19 | import org.jooq.Sequence
20 | import org.jooq.Table
21 | import org.jooq.impl.SchemaImpl
22 |
23 |
24 | object Public {
25 |
26 | /**
27 | * The reference instance of public
28 | */
29 | val PUBLIC = new Public
30 | }
31 |
32 | /**
33 | * This class is generated by jOOQ.
34 | */
35 | @Generated(
36 | value = Array(
37 | "http://www.jooq.org",
38 | "jOOQ version:3.7.0"
39 | ),
40 | comments = "This class is generated by jOOQ"
41 | )
42 | class Public extends SchemaImpl("public") {
43 |
44 | override def getSequences : List[Sequence[_]] = {
45 | val result = new ArrayList[Sequence[_]]
46 | result.addAll(getSequences0)
47 | result
48 | }
49 |
50 | private def getSequences0() : List[Sequence[_]] = {
51 | return Arrays.asList[Sequence[_]](
52 | Sequences.MENTION_SUBSCRIPTIONS_ID_SEQ,
53 | Sequences.MENTIONS_ID_SEQ,
54 | Sequences.TWITTER_USER_ID_SEQ,
55 | Sequences.USER_ID_SEQ)
56 | }
57 |
58 | override def getTables : List[Table[_]] = {
59 | val result = new ArrayList[Table[_]]
60 | result.addAll(getTables0)
61 | result
62 | }
63 |
64 | private def getTables0() : List[Table[_]] = {
65 | return Arrays.asList[Table[_]](
66 | MentionSubscriptions.MENTION_SUBSCRIPTIONS,
67 | Mentions.MENTIONS,
68 | PlayEvolutions.PLAY_EVOLUTIONS,
69 | TwitterUser.TWITTER_USER,
70 | User.USER)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/CH07/app/generated/Sequences.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import java.lang.Long
8 |
9 | import javax.annotation.Generated
10 |
11 | import org.jooq.Sequence
12 | import org.jooq.impl.SequenceImpl
13 |
14 |
15 | /**
16 | * Convenience access to all sequences in public
17 | */
18 | @Generated(
19 | value = Array(
20 | "http://www.jooq.org",
21 | "jOOQ version:3.7.0"
22 | ),
23 | comments = "This class is generated by jOOQ"
24 | )
25 | object Sequences {
26 |
27 | /**
28 | * The sequence public.mention_subscriptions_id_seq
29 | */
30 | val MENTION_SUBSCRIPTIONS_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("mention_subscriptions_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
31 |
32 | /**
33 | * The sequence public.mentions_id_seq
34 | */
35 | val MENTIONS_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("mentions_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
36 |
37 | /**
38 | * The sequence public.twitter_user_id_seq
39 | */
40 | val TWITTER_USER_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("twitter_user_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
41 |
42 | /**
43 | * The sequence public.user_id_seq
44 | */
45 | val USER_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("user_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
46 | }
47 |
--------------------------------------------------------------------------------
/CH07/app/generated/Tables.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 |
13 | import javax.annotation.Generated
14 |
15 |
16 | /**
17 | * Convenience access to all tables in public
18 | */
19 | @Generated(
20 | value = Array(
21 | "http://www.jooq.org",
22 | "jOOQ version:3.7.0"
23 | ),
24 | comments = "This class is generated by jOOQ"
25 | )
26 | object Tables {
27 |
28 | /**
29 | * The table public.mention_subscriptions
30 | */
31 | val MENTION_SUBSCRIPTIONS = generated.tables.MentionSubscriptions.MENTION_SUBSCRIPTIONS
32 |
33 | /**
34 | * The table public.mentions
35 | */
36 | val MENTIONS = generated.tables.Mentions.MENTIONS
37 |
38 | /**
39 | * The table public.play_evolutions
40 | */
41 | val PLAY_EVOLUTIONS = generated.tables.PlayEvolutions.PLAY_EVOLUTIONS
42 |
43 | /**
44 | * The table public.twitter_user
45 | */
46 | val TWITTER_USER = generated.tables.TwitterUser.TWITTER_USER
47 |
48 | /**
49 | * The table public.user
50 | */
51 | val USER = generated.tables.User.USER
52 | }
53 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/MentionSubscriptions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.MentionSubscriptionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object MentionSubscriptions {
29 |
30 | /**
31 | * The reference instance of public.mention_subscriptions
32 | */
33 | val MENTION_SUBSCRIPTIONS = new MentionSubscriptions
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class MentionSubscriptions(alias : String, aliased : Table[MentionSubscriptionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[MentionSubscriptionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[MentionSubscriptionsRecord] = {
52 | classOf[MentionSubscriptionsRecord]
53 | }
54 |
55 | /**
56 | * The column public.mention_subscriptions.id
.
57 | */
58 | val ID : TableField[MentionSubscriptionsRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.mention_subscriptions.created_on
.
62 | */
63 | val CREATED_ON : TableField[MentionSubscriptionsRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
64 |
65 | /**
66 | * The column public.mention_subscriptions.user_id
.
67 | */
68 | val USER_ID : TableField[MentionSubscriptionsRecord, Long] = createField("user_id", org.jooq.impl.SQLDataType.BIGINT.nullable(false), "")
69 |
70 | /**
71 | * Create a public.mention_subscriptions
table reference
72 | */
73 | def this() = {
74 | this("mention_subscriptions", null, null)
75 | }
76 |
77 | /**
78 | * Create an aliased public.mention_subscriptions
table reference
79 | */
80 | def this(alias : String) = {
81 | this(alias, generated.tables.MentionSubscriptions.MENTION_SUBSCRIPTIONS, null)
82 | }
83 |
84 | private def this(alias : String, aliased : Table[MentionSubscriptionsRecord]) = {
85 | this(alias, aliased, null)
86 | }
87 |
88 | override def getIdentity : Identity[MentionSubscriptionsRecord, Long] = {
89 | Keys.IDENTITY_MENTION_SUBSCRIPTIONS
90 | }
91 |
92 | override def getPrimaryKey : UniqueKey[MentionSubscriptionsRecord] = {
93 | Keys.MENTION_SUBSCRIPTIONS_PKEY
94 | }
95 |
96 | override def getKeys : List[ UniqueKey[MentionSubscriptionsRecord] ] = {
97 | return Arrays.asList[ UniqueKey[MentionSubscriptionsRecord] ](Keys.MENTION_SUBSCRIPTIONS_PKEY)
98 | }
99 |
100 | override def as(alias : String) : MentionSubscriptions = {
101 | new MentionSubscriptions(alias, this)
102 | }
103 |
104 | /**
105 | * Rename this table
106 | */
107 | def rename(name : String) : MentionSubscriptions = {
108 | new MentionSubscriptions(name, null)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/Mentions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.MentionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object Mentions {
29 |
30 | /**
31 | * The reference instance of public.mentions
32 | */
33 | val MENTIONS = new Mentions
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class Mentions(alias : String, aliased : Table[MentionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[MentionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[MentionsRecord] = {
52 | classOf[MentionsRecord]
53 | }
54 |
55 | /**
56 | * The column public.mentions.id
.
57 | */
58 | val ID : TableField[MentionsRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.mentions.tweet_id
.
62 | */
63 | val TWEET_ID : TableField[MentionsRecord, String] = createField("tweet_id", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
64 |
65 | /**
66 | * The column public.mentions.user_id
.
67 | */
68 | val USER_ID : TableField[MentionsRecord, Long] = createField("user_id", org.jooq.impl.SQLDataType.BIGINT.nullable(false), "")
69 |
70 | /**
71 | * The column public.mentions.created_on
.
72 | */
73 | val CREATED_ON : TableField[MentionsRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
74 |
75 | /**
76 | * The column public.mentions.author_user_name
.
77 | */
78 | val AUTHOR_USER_NAME : TableField[MentionsRecord, String] = createField("author_user_name", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
79 |
80 | /**
81 | * The column public.mentions.text
.
82 | */
83 | val TEXT : TableField[MentionsRecord, String] = createField("text", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
84 |
85 | /**
86 | * Create a public.mentions
table reference
87 | */
88 | def this() = {
89 | this("mentions", null, null)
90 | }
91 |
92 | /**
93 | * Create an aliased public.mentions
table reference
94 | */
95 | def this(alias : String) = {
96 | this(alias, generated.tables.Mentions.MENTIONS, null)
97 | }
98 |
99 | private def this(alias : String, aliased : Table[MentionsRecord]) = {
100 | this(alias, aliased, null)
101 | }
102 |
103 | override def getIdentity : Identity[MentionsRecord, Long] = {
104 | Keys.IDENTITY_MENTIONS
105 | }
106 |
107 | override def getPrimaryKey : UniqueKey[MentionsRecord] = {
108 | Keys.MENTIONS_PKEY
109 | }
110 |
111 | override def getKeys : List[ UniqueKey[MentionsRecord] ] = {
112 | return Arrays.asList[ UniqueKey[MentionsRecord] ](Keys.MENTIONS_PKEY)
113 | }
114 |
115 | override def as(alias : String) : Mentions = {
116 | new Mentions(alias, this)
117 | }
118 |
119 | /**
120 | * Rename this table
121 | */
122 | def rename(name : String) : Mentions = {
123 | new Mentions(name, null)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/PlayEvolutions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.PlayEvolutionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Integer
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Table
22 | import org.jooq.TableField
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.TableImpl
25 |
26 |
27 | object PlayEvolutions {
28 |
29 | /**
30 | * The reference instance of public.play_evolutions
31 | */
32 | val PLAY_EVOLUTIONS = new PlayEvolutions
33 | }
34 |
35 | /**
36 | * This class is generated by jOOQ.
37 | */
38 | @Generated(
39 | value = Array(
40 | "http://www.jooq.org",
41 | "jOOQ version:3.7.0"
42 | ),
43 | comments = "This class is generated by jOOQ"
44 | )
45 | class PlayEvolutions(alias : String, aliased : Table[PlayEvolutionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[PlayEvolutionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
46 |
47 | /**
48 | * The class holding records for this type
49 | */
50 | override def getRecordType : Class[PlayEvolutionsRecord] = {
51 | classOf[PlayEvolutionsRecord]
52 | }
53 |
54 | /**
55 | * The column public.play_evolutions.id
.
56 | */
57 | val ID : TableField[PlayEvolutionsRecord, Integer] = createField("id", org.jooq.impl.SQLDataType.INTEGER.nullable(false), "")
58 |
59 | /**
60 | * The column public.play_evolutions.hash
.
61 | */
62 | val HASH : TableField[PlayEvolutionsRecord, String] = createField("hash", org.jooq.impl.SQLDataType.VARCHAR.length(255).nullable(false), "")
63 |
64 | /**
65 | * The column public.play_evolutions.applied_at
.
66 | */
67 | val APPLIED_AT : TableField[PlayEvolutionsRecord, Timestamp] = createField("applied_at", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
68 |
69 | /**
70 | * The column public.play_evolutions.apply_script
.
71 | */
72 | val APPLY_SCRIPT : TableField[PlayEvolutionsRecord, String] = createField("apply_script", org.jooq.impl.SQLDataType.CLOB, "")
73 |
74 | /**
75 | * The column public.play_evolutions.revert_script
.
76 | */
77 | val REVERT_SCRIPT : TableField[PlayEvolutionsRecord, String] = createField("revert_script", org.jooq.impl.SQLDataType.CLOB, "")
78 |
79 | /**
80 | * The column public.play_evolutions.state
.
81 | */
82 | val STATE : TableField[PlayEvolutionsRecord, String] = createField("state", org.jooq.impl.SQLDataType.VARCHAR.length(255), "")
83 |
84 | /**
85 | * The column public.play_evolutions.last_problem
.
86 | */
87 | val LAST_PROBLEM : TableField[PlayEvolutionsRecord, String] = createField("last_problem", org.jooq.impl.SQLDataType.CLOB, "")
88 |
89 | /**
90 | * Create a public.play_evolutions
table reference
91 | */
92 | def this() = {
93 | this("play_evolutions", null, null)
94 | }
95 |
96 | /**
97 | * Create an aliased public.play_evolutions
table reference
98 | */
99 | def this(alias : String) = {
100 | this(alias, generated.tables.PlayEvolutions.PLAY_EVOLUTIONS, null)
101 | }
102 |
103 | private def this(alias : String, aliased : Table[PlayEvolutionsRecord]) = {
104 | this(alias, aliased, null)
105 | }
106 |
107 | override def getPrimaryKey : UniqueKey[PlayEvolutionsRecord] = {
108 | Keys.PLAY_EVOLUTIONS_PKEY
109 | }
110 |
111 | override def getKeys : List[ UniqueKey[PlayEvolutionsRecord] ] = {
112 | return Arrays.asList[ UniqueKey[PlayEvolutionsRecord] ](Keys.PLAY_EVOLUTIONS_PKEY)
113 | }
114 |
115 | override def as(alias : String) : PlayEvolutions = {
116 | new PlayEvolutions(alias, this)
117 | }
118 |
119 | /**
120 | * Rename this table
121 | */
122 | def rename(name : String) : PlayEvolutions = {
123 | new PlayEvolutions(name, null)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/TwitterUser.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.TwitterUserRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object TwitterUser {
29 |
30 | /**
31 | * The reference instance of public.twitter_user
32 | */
33 | val TWITTER_USER = new TwitterUser
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class TwitterUser(alias : String, aliased : Table[TwitterUserRecord], parameters : Array[ Field[_] ]) extends TableImpl[TwitterUserRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[TwitterUserRecord] = {
52 | classOf[TwitterUserRecord]
53 | }
54 |
55 | /**
56 | * The column public.twitter_user.id
.
57 | */
58 | val ID : TableField[TwitterUserRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.twitter_user.created_on
.
62 | */
63 | val CREATED_ON : TableField[TwitterUserRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
64 |
65 | /**
66 | * The column public.twitter_user.phone_number
.
67 | */
68 | val PHONE_NUMBER : TableField[TwitterUserRecord, String] = createField("phone_number", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
69 |
70 | /**
71 | * The column public.twitter_user.twitter_user_name
.
72 | */
73 | val TWITTER_USER_NAME : TableField[TwitterUserRecord, String] = createField("twitter_user_name", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
74 |
75 | /**
76 | * Create a public.twitter_user
table reference
77 | */
78 | def this() = {
79 | this("twitter_user", null, null)
80 | }
81 |
82 | /**
83 | * Create an aliased public.twitter_user
table reference
84 | */
85 | def this(alias : String) = {
86 | this(alias, generated.tables.TwitterUser.TWITTER_USER, null)
87 | }
88 |
89 | private def this(alias : String, aliased : Table[TwitterUserRecord]) = {
90 | this(alias, aliased, null)
91 | }
92 |
93 | override def getIdentity : Identity[TwitterUserRecord, Long] = {
94 | Keys.IDENTITY_TWITTER_USER
95 | }
96 |
97 | override def getPrimaryKey : UniqueKey[TwitterUserRecord] = {
98 | Keys.TWITTER_USER_PKEY
99 | }
100 |
101 | override def getKeys : List[ UniqueKey[TwitterUserRecord] ] = {
102 | return Arrays.asList[ UniqueKey[TwitterUserRecord] ](Keys.TWITTER_USER_PKEY)
103 | }
104 |
105 | override def as(alias : String) : TwitterUser = {
106 | new TwitterUser(alias, this)
107 | }
108 |
109 | /**
110 | * Rename this table
111 | */
112 | def rename(name : String) : TwitterUser = {
113 | new TwitterUser(name, null)
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/User.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.UserRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.util.Arrays
15 | import java.util.List
16 |
17 | import javax.annotation.Generated
18 |
19 | import org.jooq.Field
20 | import org.jooq.Identity
21 | import org.jooq.Table
22 | import org.jooq.TableField
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.TableImpl
25 |
26 |
27 | object User {
28 |
29 | /**
30 | * The reference instance of public.user
31 | */
32 | val USER = new User
33 | }
34 |
35 | /**
36 | * This class is generated by jOOQ.
37 | */
38 | @Generated(
39 | value = Array(
40 | "http://www.jooq.org",
41 | "jOOQ version:3.7.0"
42 | ),
43 | comments = "This class is generated by jOOQ"
44 | )
45 | class User(alias : String, aliased : Table[UserRecord], parameters : Array[ Field[_] ]) extends TableImpl[UserRecord](alias, Public.PUBLIC, aliased, parameters, "") {
46 |
47 | /**
48 | * The class holding records for this type
49 | */
50 | override def getRecordType : Class[UserRecord] = {
51 | classOf[UserRecord]
52 | }
53 |
54 | /**
55 | * The column public.user.id
.
56 | */
57 | val ID : TableField[UserRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
58 |
59 | /**
60 | * The column public.user.email
.
61 | */
62 | val EMAIL : TableField[UserRecord, String] = createField("email", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
63 |
64 | /**
65 | * The column public.user.password
.
66 | */
67 | val PASSWORD : TableField[UserRecord, String] = createField("password", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
68 |
69 | /**
70 | * The column public.user.firstname
.
71 | */
72 | val FIRSTNAME : TableField[UserRecord, String] = createField("firstname", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
73 |
74 | /**
75 | * The column public.user.lastname
.
76 | */
77 | val LASTNAME : TableField[UserRecord, String] = createField("lastname", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
78 |
79 | /**
80 | * Create a public.user
table reference
81 | */
82 | def this() = {
83 | this("user", null, null)
84 | }
85 |
86 | /**
87 | * Create an aliased public.user
table reference
88 | */
89 | def this(alias : String) = {
90 | this(alias, generated.tables.User.USER, null)
91 | }
92 |
93 | private def this(alias : String, aliased : Table[UserRecord]) = {
94 | this(alias, aliased, null)
95 | }
96 |
97 | override def getIdentity : Identity[UserRecord, Long] = {
98 | Keys.IDENTITY_USER
99 | }
100 |
101 | override def getPrimaryKey : UniqueKey[UserRecord] = {
102 | Keys.USER_PKEY
103 | }
104 |
105 | override def getKeys : List[ UniqueKey[UserRecord] ] = {
106 | return Arrays.asList[ UniqueKey[UserRecord] ](Keys.USER_PKEY)
107 | }
108 |
109 | override def as(alias : String) : User = {
110 | new User(alias, this)
111 | }
112 |
113 | /**
114 | * Rename this table
115 | */
116 | def rename(name : String) : User = {
117 | new User(name, null)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/records/MentionSubscriptionsRecord.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables.records
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 |
9 | import java.lang.Long
10 | import java.sql.Timestamp
11 |
12 | import javax.annotation.Generated
13 |
14 | import org.jooq.Field
15 | import org.jooq.Record1
16 | import org.jooq.Record3
17 | import org.jooq.Row3
18 | import org.jooq.impl.UpdatableRecordImpl
19 |
20 |
21 | /**
22 | * This class is generated by jOOQ.
23 | */
24 | @Generated(
25 | value = Array(
26 | "http://www.jooq.org",
27 | "jOOQ version:3.7.0"
28 | ),
29 | comments = "This class is generated by jOOQ"
30 | )
31 | class MentionSubscriptionsRecord extends UpdatableRecordImpl[MentionSubscriptionsRecord](MentionSubscriptions.MENTION_SUBSCRIPTIONS) with Record3[Long, Timestamp, Long] {
32 |
33 | /**
34 | * Setter for public.mention_subscriptions.id
.
35 | */
36 | def setId(value : Long) : Unit = {
37 | setValue(0, value)
38 | }
39 |
40 | /**
41 | * Getter for public.mention_subscriptions.id
.
42 | */
43 | def getId : Long = {
44 | val r = getValue(0)
45 | if (r == null) null else r.asInstanceOf[Long]
46 | }
47 |
48 | /**
49 | * Setter for public.mention_subscriptions.created_on
.
50 | */
51 | def setCreatedOn(value : Timestamp) : Unit = {
52 | setValue(1, value)
53 | }
54 |
55 | /**
56 | * Getter for public.mention_subscriptions.created_on
.
57 | */
58 | def getCreatedOn : Timestamp = {
59 | val r = getValue(1)
60 | if (r == null) null else r.asInstanceOf[Timestamp]
61 | }
62 |
63 | /**
64 | * Setter for public.mention_subscriptions.user_id
.
65 | */
66 | def setUserId(value : Long) : Unit = {
67 | setValue(2, value)
68 | }
69 |
70 | /**
71 | * Getter for public.mention_subscriptions.user_id
.
72 | */
73 | def getUserId : Long = {
74 | val r = getValue(2)
75 | if (r == null) null else r.asInstanceOf[Long]
76 | }
77 |
78 | // -------------------------------------------------------------------------
79 | // Primary key information
80 | // -------------------------------------------------------------------------
81 | override def key() : Record1[Long] = {
82 | return super.key.asInstanceOf[ Record1[Long] ]
83 | }
84 |
85 | // -------------------------------------------------------------------------
86 | // Record3 type implementation
87 | // -------------------------------------------------------------------------
88 |
89 | override def fieldsRow : Row3[Long, Timestamp, Long] = {
90 | super.fieldsRow.asInstanceOf[ Row3[Long, Timestamp, Long] ]
91 | }
92 |
93 | override def valuesRow : Row3[Long, Timestamp, Long] = {
94 | super.valuesRow.asInstanceOf[ Row3[Long, Timestamp, Long] ]
95 | }
96 | override def field1 : Field[Long] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID
97 | override def field2 : Field[Timestamp] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.CREATED_ON
98 | override def field3 : Field[Long] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.USER_ID
99 | override def value1 : Long = getId
100 | override def value2 : Timestamp = getCreatedOn
101 | override def value3 : Long = getUserId
102 |
103 | override def value1(value : Long) : MentionSubscriptionsRecord = {
104 | setId(value)
105 | this
106 | }
107 |
108 | override def value2(value : Timestamp) : MentionSubscriptionsRecord = {
109 | setCreatedOn(value)
110 | this
111 | }
112 |
113 | override def value3(value : Long) : MentionSubscriptionsRecord = {
114 | setUserId(value)
115 | this
116 | }
117 |
118 | override def values(value1 : Long, value2 : Timestamp, value3 : Long) : MentionSubscriptionsRecord = {
119 | this.value1(value1)
120 | this.value2(value2)
121 | this.value3(value3)
122 | this
123 | }
124 |
125 | /**
126 | * Create a detached, initialised MentionSubscriptionsRecord
127 | */
128 | def this(id : Long, createdOn : Timestamp, userId : Long) = {
129 | this()
130 |
131 | setValue(0, id)
132 | setValue(1, createdOn)
133 | setValue(2, userId)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/CH07/app/generated/tables/records/TwitterUserRecord.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables.records
5 |
6 |
7 | import generated.tables.TwitterUser
8 |
9 | import java.lang.Long
10 | import java.lang.String
11 | import java.sql.Timestamp
12 |
13 | import javax.annotation.Generated
14 |
15 | import org.jooq.Field
16 | import org.jooq.Record1
17 | import org.jooq.Record4
18 | import org.jooq.Row4
19 | import org.jooq.impl.UpdatableRecordImpl
20 |
21 |
22 | /**
23 | * This class is generated by jOOQ.
24 | */
25 | @Generated(
26 | value = Array(
27 | "http://www.jooq.org",
28 | "jOOQ version:3.7.0"
29 | ),
30 | comments = "This class is generated by jOOQ"
31 | )
32 | class TwitterUserRecord extends UpdatableRecordImpl[TwitterUserRecord](TwitterUser.TWITTER_USER) with Record4[Long, Timestamp, String, String] {
33 |
34 | /**
35 | * Setter for public.twitter_user.id
.
36 | */
37 | def setId(value : Long) : Unit = {
38 | setValue(0, value)
39 | }
40 |
41 | /**
42 | * Getter for public.twitter_user.id
.
43 | */
44 | def getId : Long = {
45 | val r = getValue(0)
46 | if (r == null) null else r.asInstanceOf[Long]
47 | }
48 |
49 | /**
50 | * Setter for public.twitter_user.created_on
.
51 | */
52 | def setCreatedOn(value : Timestamp) : Unit = {
53 | setValue(1, value)
54 | }
55 |
56 | /**
57 | * Getter for public.twitter_user.created_on
.
58 | */
59 | def getCreatedOn : Timestamp = {
60 | val r = getValue(1)
61 | if (r == null) null else r.asInstanceOf[Timestamp]
62 | }
63 |
64 | /**
65 | * Setter for public.twitter_user.phone_number
.
66 | */
67 | def setPhoneNumber(value : String) : Unit = {
68 | setValue(2, value)
69 | }
70 |
71 | /**
72 | * Getter for public.twitter_user.phone_number
.
73 | */
74 | def getPhoneNumber : String = {
75 | val r = getValue(2)
76 | if (r == null) null else r.asInstanceOf[String]
77 | }
78 |
79 | /**
80 | * Setter for public.twitter_user.twitter_user_name
.
81 | */
82 | def setTwitterUserName(value : String) : Unit = {
83 | setValue(3, value)
84 | }
85 |
86 | /**
87 | * Getter for public.twitter_user.twitter_user_name
.
88 | */
89 | def getTwitterUserName : String = {
90 | val r = getValue(3)
91 | if (r == null) null else r.asInstanceOf[String]
92 | }
93 |
94 | // -------------------------------------------------------------------------
95 | // Primary key information
96 | // -------------------------------------------------------------------------
97 | override def key() : Record1[Long] = {
98 | return super.key.asInstanceOf[ Record1[Long] ]
99 | }
100 |
101 | // -------------------------------------------------------------------------
102 | // Record4 type implementation
103 | // -------------------------------------------------------------------------
104 |
105 | override def fieldsRow : Row4[Long, Timestamp, String, String] = {
106 | super.fieldsRow.asInstanceOf[ Row4[Long, Timestamp, String, String] ]
107 | }
108 |
109 | override def valuesRow : Row4[Long, Timestamp, String, String] = {
110 | super.valuesRow.asInstanceOf[ Row4[Long, Timestamp, String, String] ]
111 | }
112 | override def field1 : Field[Long] = TwitterUser.TWITTER_USER.ID
113 | override def field2 : Field[Timestamp] = TwitterUser.TWITTER_USER.CREATED_ON
114 | override def field3 : Field[String] = TwitterUser.TWITTER_USER.PHONE_NUMBER
115 | override def field4 : Field[String] = TwitterUser.TWITTER_USER.TWITTER_USER_NAME
116 | override def value1 : Long = getId
117 | override def value2 : Timestamp = getCreatedOn
118 | override def value3 : String = getPhoneNumber
119 | override def value4 : String = getTwitterUserName
120 |
121 | override def value1(value : Long) : TwitterUserRecord = {
122 | setId(value)
123 | this
124 | }
125 |
126 | override def value2(value : Timestamp) : TwitterUserRecord = {
127 | setCreatedOn(value)
128 | this
129 | }
130 |
131 | override def value3(value : String) : TwitterUserRecord = {
132 | setPhoneNumber(value)
133 | this
134 | }
135 |
136 | override def value4(value : String) : TwitterUserRecord = {
137 | setTwitterUserName(value)
138 | this
139 | }
140 |
141 | override def values(value1 : Long, value2 : Timestamp, value3 : String, value4 : String) : TwitterUserRecord = {
142 | this.value1(value1)
143 | this.value2(value2)
144 | this.value3(value3)
145 | this.value4(value4)
146 | this
147 | }
148 |
149 | /**
150 | * Create a detached, initialised TwitterUserRecord
151 | */
152 | def this(id : Long, createdOn : Timestamp, phoneNumber : String, twitterUserName : String) = {
153 | this()
154 |
155 | setValue(0, id)
156 | setValue(1, createdOn)
157 | setValue(2, phoneNumber)
158 | setValue(3, twitterUserName)
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/CH07/app/helpers/Contexts.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import play.api.Play.current
4 | import play.api.libs.concurrent.Akka
5 | import scala.concurrent.ExecutionContext
6 |
7 |
8 | object Contexts {
9 | val database: ExecutionContext =
10 | Akka.system.dispatchers.lookup("contexts.database")
11 | }
--------------------------------------------------------------------------------
/CH07/app/helpers/Database.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import javax.inject.Inject
4 |
5 | import org.jooq.{DSLContext, SQLDialect}
6 | import org.jooq.impl.DSL
7 | import scala.concurrent.Future
8 |
9 | class Database @Inject() (db: play.api.db.Database) {
10 |
11 | def query[A](block: DSLContext => A): Future[A] = Future {
12 | db.withConnection { connection =>
13 | val sql = DSL.using(connection, SQLDialect.POSTGRES_9_4)
14 | block(sql)
15 | }
16 | }(Contexts.database)
17 |
18 | def withTransaction[A](block: DSLContext => A): Future[A] = Future {
19 | db.withTransaction { connection =>
20 | val sql = DSL.using(connection, SQLDialect.POSTGRES_9_4)
21 | block(sql)
22 | }
23 | }(Contexts.database)
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/CH07/app/helpers/TwitterCredentials.scala:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import play.api.Play
4 | import play.api.libs.oauth.{RequestToken, ConsumerKey}
5 |
6 | trait TwitterCredentials {
7 |
8 | import play.api.Play.current
9 |
10 | protected def credentials = for {
11 | apiKey <- Play.configuration.getString("twitter.apiKey")
12 | apiSecret <- Play.configuration.getString("twitter.apiSecret")
13 | token <- Play.configuration.getString("twitter.accessToken")
14 | tokenSecret <- Play.configuration.getString("twitter.accessTokenSecret")
15 | } yield (ConsumerKey(apiKey, apiSecret), RequestToken(token, tokenSecret))
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/CH07/app/modules/Fixtures.scala:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import javax.inject.Inject
4 |
5 | import com.google.inject.AbstractModule
6 | import generated.Tables._
7 | import org.jooq.SQLDialect
8 | import org.jooq.impl.DSL
9 | import play.api.db.Database
10 | import play.api.libs.Crypto
11 |
12 | class Fixtures @Inject() (val crypto: Crypto, db: Database) extends DatabaseFixtures{
13 | db.withTransaction { connection =>
14 | val sql = DSL.using(connection, SQLDialect.POSTGRES_9_4)
15 | if (sql.fetchCount(USER) == 0) {
16 | sql
17 | .insertInto(USER)
18 | .columns(USER.EMAIL, USER.FIRSTNAME, USER.LASTNAME, USER.PASSWORD)
19 | .values("bob@marley.org", "Bob", "Marley", crypto.sign("secret"))
20 | .execute()
21 | }
22 |
23 | }
24 | }
25 |
26 | trait DatabaseFixtures
27 |
28 | class FixturesModule extends AbstractModule {
29 | override def configure(): Unit = {
30 | bind(classOf[DatabaseFixtures]).to(classOf[Fixtures]).asEagerSingleton
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/CH07/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(firstName: String)
2 |
3 | Hello @firstName !
--------------------------------------------------------------------------------
/CH07/app/views/login.scala.html:
--------------------------------------------------------------------------------
1 | @(form: Form[(String, String)])(implicit messages: Messages)
2 |
3 | @form.globalError.map { error =>
4 | @error.message
5 | }
6 |
7 | @helper.form(controllers.routes.Application.authenticate()) {
8 | @helper.inputText(form("email"))
9 | @helper.inputPassword(form("password"))
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/CH07/build.sbt:
--------------------------------------------------------------------------------
1 | name := "CH07"
2 |
3 | version := "1.0"
4 |
5 | lazy val `ch07` = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.7"
8 |
9 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2"
10 |
11 | libraryDependencies ++= Seq(
12 | jdbc,
13 | cache,
14 | ws,
15 | evolutions,
16 | "com.github.mumoshu" %% "play2-memcached-play24" % "0.7.0",
17 | "org.postgresql" % "postgresql" % "9.4-1201-jdbc41",
18 | "org.jooq" % "jooq" % "3.7.0",
19 | "org.jooq" % "jooq-codegen-maven" % "3.7.0",
20 | "org.jooq" % "jooq-meta" % "3.7.0",
21 | "joda-time" % "joda-time" % "2.7",
22 | "com.github.ironfish" %% "akka-persistence-mongo-casbah" % "0.7.6"
23 | )
24 |
25 | routesGenerator := InjectedRoutesGenerator
26 |
27 | val generateJOOQ = taskKey[Seq[File]]("Generate JooQ classes")
28 |
29 | val generateJOOQTask = (baseDirectory, dependencyClasspath in Compile, runner in Compile, streams) map { (base, cp, r, s) =>
30 | toError(r.run(
31 | "org.jooq.util.GenerationTool",
32 | cp.files,
33 | Array("conf/chapter7.xml"),
34 | s.log))
35 | ((base / "app" / "generated") ** "*.scala").get
36 | }
37 |
38 | generateJOOQ <<= generateJOOQTask
39 |
40 | libraryDependencies += "com.ning" % "async-http-client" % "1.9.29"
41 |
--------------------------------------------------------------------------------
/CH07/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.crypto.secret="b@qgnBVvLh]HV;y[6zuu>1Wm?q5a`1Blt9j`WH953moD<_w7UIn2KlPzND[f=;1L"
2 | play.i18n.langs = ["en"]
3 |
4 | ### Database configuration
5 |
6 | db.default.driver="org.postgresql.Driver"
7 | db.default.url="jdbc:postgresql://localhost/chapter7"
8 | db.default.user=user
9 | db.default.password=secret
10 | db.default.maximumPoolSize = 9
11 |
12 | contexts {
13 | database {
14 | fork-join-executor {
15 | parallelism-max = 9
16 | }
17 | }
18 | }
19 |
20 | ### Akka logging
21 |
22 | akka {
23 | loggers = ["akka.event.slf4j.Slf4jLogger"]
24 | loglevel = "DEBUG"
25 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
26 | }
27 |
28 |
29 | ### Application modules
30 |
31 | play.modules.enabled += "actors.SMSServiceModule"
32 | play.modules.enabled += "modules.FixturesModule"
33 |
34 | ### Cache configuration
35 |
36 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule"
37 |
38 | # To avoid conflict with Play's built-in cache module
39 | play.modules.disabled+="play.api.cache.EhCacheModule"
40 |
41 | # Well-known configuration provided by Play
42 | play.modules.cache.defaultCache=default
43 | play.modules.cache.bindCaches=["db-cache", "user-cache", "session-cache"]
44 |
45 | # Tell play2-memcached where your memcached host is located at
46 | memcached.host="127.0.0.1:11211"
47 |
48 | ### Akka Persistence
49 |
50 | akka.persistence.journal.plugin = "casbah-journal"
51 | casbah-journal.mongo-journal-url = "mongodb://localhost:27017/sms-event-store.messages"
52 | casbah-journal.mongo-journal-write-concern = "journaled"
53 |
54 | akka.persistence.snapshot-store.plugin = "casbah-snapshot-store"
55 | casbah-snapshot-store.mongo-snapshot-url = "mongodb://localhost:27017/sms-event-store.snapshots"
56 | casbah-snapshot-store.mongo-snapshot-write-concern = "journaled"
57 |
58 | include "twitter.conf"
--------------------------------------------------------------------------------
/CH07/conf/chapter7.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | org.postgresql.Driver
5 | jdbc:postgresql://localhost/chapter7
6 | user
7 | secret
8 |
9 |
10 | org.jooq.util.ScalaGenerator
11 |
12 | org.jooq.util.postgres.PostgresDatabase
13 | public
14 | .*
15 |
16 |
17 |
18 | generated
19 | app
20 |
21 |
22 |
--------------------------------------------------------------------------------
/CH07/conf/evolutions/default/1.sql:
--------------------------------------------------------------------------------
1 | # --- !Ups
2 |
3 | CREATE TABLE "user" (
4 | id bigserial primary key,
5 | email varchar NOT NULL,
6 | password varchar NOT NULL,
7 | firstname varchar NOT NULL,
8 | lastname varchar NOT NULL
9 | );
10 |
11 | # --- !Downs
12 |
13 | DROP TABLE "user";
14 |
--------------------------------------------------------------------------------
/CH07/conf/evolutions/default/2.sql:
--------------------------------------------------------------------------------
1 | # --- !Ups
2 |
3 | CREATE TABLE "twitter_user" (
4 | id bigserial primary key,
5 | created_on timestamp with time zone NOT NULL,
6 | phone_number varchar NOT NULL,
7 | twitter_user_name varchar NOT NULL
8 | );
9 |
10 | CREATE TABLE "mentions" (
11 | id bigserial primary key,
12 | tweet_id varchar NOT NULL,
13 | user_id bigint NOT NULL,
14 | created_on timestamp with time zone NOT NULL,
15 | author_user_name varchar NOT NULL,
16 | text varchar NOT NULL
17 | );
18 |
19 | CREATE TABLE "mention_subscriptions" (
20 | id bigserial primary key,
21 | created_on timestamp with time zone NOT NULL,
22 | user_id bigint NOT NULL
23 | );
24 |
25 | # --- !Downs
26 |
27 | DROP TABLE "twitter_user";
28 | DROP TABLE "mentions";
29 | DROP TABLE "mention_subscriptions";
30 |
--------------------------------------------------------------------------------
/CH07/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/CH07/conf/routes:
--------------------------------------------------------------------------------
1 | GET / controllers.Application.index()
2 | GET /login controllers.Application.login()
3 | GET /logout controllers.Application.logout()
4 | POST /authenticate controllers.Application.authenticate()
5 |
--------------------------------------------------------------------------------
/CH07/conf/twitter.conf:
--------------------------------------------------------------------------------
1 | # Twitter
2 | twitter.apiKey=""
3 | twitter.apiSecret=""
4 | twitter.accessToken=""
5 | twitter.accessTokenSecret=""
6 |
--------------------------------------------------------------------------------
/CH07/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/CH07/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
--------------------------------------------------------------------------------
/CH08/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 |
--------------------------------------------------------------------------------
/CH08/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import java.sql.Timestamp
4 |
5 | import akka.actor.{Actor, ActorRef, Props}
6 | import dashboard.GraphType
7 | import generated.Tables._
8 | import org.joda.time.format.DateTimeFormat
9 | import org.joda.time.LocalDate
10 | import org.jooq.impl.DSL._
11 | import org.jooq.types.YearToMonth
12 | import org.jooq.{Converter, DatePart, SQLDialect}
13 | import play.api.Play.current
14 | import play.api.db._
15 | import play.api.libs.json.{JsValue, Json}
16 | import play.api.mvc._
17 |
18 | object Application extends Controller {
19 |
20 | def index = Action {
21 | Ok(views.html.index())
22 | }
23 |
24 | def graphs = WebSocket.acceptWithActor[String, JsValue] {
25 | request => out => DashboardClient.props(out)
26 | }
27 |
28 | }
29 |
30 | class DashboardClient(out: ActorRef) extends Actor {
31 | def graphType(s: String) = GraphType.withName(s)
32 | def receive = {
33 | case t: String => graphType(t) match {
34 | case GraphType.MonthlySubscriptions =>
35 | val mentionsCount = DB.withConnection { connection =>
36 | val sql = using(connection, SQLDialect.POSTGRES_9_4)
37 |
38 |
39 | // TODO do everything in the query once jOOQ supports timestamps in generate_series
40 | /*
41 | SELECT *
42 | FROM (SELECT generate_series(now() - '1 month'::interval, now(), '1 day'::interval)::date) AS d(day)
43 | LEFT JOIN (
44 | SELECT date_trunc('day', created_on)::date AS day, count(*) AS mention_count
45 | FROM mentions
46 | WHERE created_on > now() - interval '1 month'
47 | GROUP BY 1
48 | ) t USING (day)
49 | ORDER BY 1;
50 | */
51 |
52 | sql.select(trunc(MENTIONS.CREATED_ON, DatePart.DAY).as("day"), count())
53 | .from(MENTIONS)
54 | .where(MENTIONS.CREATED_ON.greaterThan(currentTimestamp().sub(new YearToMonth(0, 1))))
55 | .groupBy(field("day"))
56 | .orderBy(field("day"))
57 | .fetch()
58 | }
59 |
60 | val allDates = (1 to 30).map { day =>
61 | LocalDate.now.minusDays(day)
62 | }
63 |
64 | import scala.collection.JavaConverters._
65 |
66 | val counts: Map[LocalDate, Int] = mentionsCount.iterator().asScala.map { record =>
67 | record.getValue(0, new LocalDateConverter) -> record.getValue(1, classOf[Int])
68 | }.toMap
69 |
70 | val monthlyCounts: Map[String, Int] = allDates.map { day =>
71 | DateTimeFormat.forPattern("dd/MM").print(day) -> counts.get(day).getOrElse(0)
72 | }.sortBy(_._1).toMap
73 |
74 |
75 | out ! Json.obj(
76 | "graph_type" -> GraphType.MonthlySubscriptions,
77 | "labels" -> Json.toJson(monthlyCounts.keys),
78 | "series" -> Json.arr("Subscriptions"),
79 | "data" -> Json.arr(Json.toJson(monthlyCounts.values))
80 | )
81 | }
82 | }
83 | }
84 | object DashboardClient {
85 | def props(out: ActorRef) = Props(classOf[DashboardClient], out)
86 | }
87 |
88 | class LocalDateConverter extends Converter[Timestamp, LocalDate] {
89 | override def from(t: Timestamp): LocalDate = new LocalDate(t)
90 | override def to(u: LocalDate): Timestamp = new Timestamp(u.toDateTimeAtStartOfDay.getMillis)
91 | override def fromType(): Class[Timestamp] = classOf[Timestamp]
92 | override def toType: Class[LocalDate] = classOf[LocalDate]
93 | }
94 |
--------------------------------------------------------------------------------
/CH08/app/generated/Keys.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 | import generated.tables.records.MentionSubscriptionsRecord
13 | import generated.tables.records.MentionsRecord
14 | import generated.tables.records.PlayEvolutionsRecord
15 | import generated.tables.records.TwitterUserRecord
16 | import generated.tables.records.UserRecord
17 |
18 | import java.lang.Long
19 |
20 | import javax.annotation.Generated
21 |
22 | import org.jooq.Identity
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.AbstractKeys
25 |
26 |
27 | /**
28 | * A class modelling foreign key relationships between tables of the public
29 | * schema
30 | */
31 | @Generated(
32 | value = Array(
33 | "http://www.jooq.org",
34 | "jOOQ version:3.7.0"
35 | ),
36 | comments = "This class is generated by jOOQ"
37 | )
38 | object Keys {
39 |
40 | // -------------------------------------------------------------------------
41 | // IDENTITY definitions
42 | // -------------------------------------------------------------------------
43 |
44 | val IDENTITY_MENTION_SUBSCRIPTIONS = Identities0.IDENTITY_MENTION_SUBSCRIPTIONS
45 | val IDENTITY_MENTIONS = Identities0.IDENTITY_MENTIONS
46 | val IDENTITY_TWITTER_USER = Identities0.IDENTITY_TWITTER_USER
47 | val IDENTITY_USER = Identities0.IDENTITY_USER
48 |
49 | // -------------------------------------------------------------------------
50 | // UNIQUE and PRIMARY KEY definitions
51 | // -------------------------------------------------------------------------
52 |
53 | val MENTION_SUBSCRIPTIONS_PKEY = UniqueKeys0.MENTION_SUBSCRIPTIONS_PKEY
54 | val MENTIONS_PKEY = UniqueKeys0.MENTIONS_PKEY
55 | val PLAY_EVOLUTIONS_PKEY = UniqueKeys0.PLAY_EVOLUTIONS_PKEY
56 | val TWITTER_USER_PKEY = UniqueKeys0.TWITTER_USER_PKEY
57 | val USER_PKEY = UniqueKeys0.USER_PKEY
58 |
59 | // -------------------------------------------------------------------------
60 | // FOREIGN KEY definitions
61 | // -------------------------------------------------------------------------
62 |
63 |
64 | // -------------------------------------------------------------------------
65 | // [#1459] distribute members to avoid static initialisers > 64kb
66 | // -------------------------------------------------------------------------
67 |
68 | private object Identities0 extends AbstractKeys {
69 | val IDENTITY_MENTION_SUBSCRIPTIONS : Identity[MentionSubscriptionsRecord, Long] = AbstractKeys.createIdentity(MentionSubscriptions.MENTION_SUBSCRIPTIONS, MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID)
70 | val IDENTITY_MENTIONS : Identity[MentionsRecord, Long] = AbstractKeys.createIdentity(Mentions.MENTIONS, Mentions.MENTIONS.ID)
71 | val IDENTITY_TWITTER_USER : Identity[TwitterUserRecord, Long] = AbstractKeys.createIdentity(TwitterUser.TWITTER_USER, TwitterUser.TWITTER_USER.ID)
72 | val IDENTITY_USER : Identity[UserRecord, Long] = AbstractKeys.createIdentity(User.USER, User.USER.ID)
73 | }
74 |
75 | private object UniqueKeys0 extends AbstractKeys {
76 | val MENTION_SUBSCRIPTIONS_PKEY : UniqueKey[MentionSubscriptionsRecord] = AbstractKeys.createUniqueKey(MentionSubscriptions.MENTION_SUBSCRIPTIONS, MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID)
77 | val MENTIONS_PKEY : UniqueKey[MentionsRecord] = AbstractKeys.createUniqueKey(Mentions.MENTIONS, Mentions.MENTIONS.ID)
78 | val PLAY_EVOLUTIONS_PKEY : UniqueKey[PlayEvolutionsRecord] = AbstractKeys.createUniqueKey(PlayEvolutions.PLAY_EVOLUTIONS, PlayEvolutions.PLAY_EVOLUTIONS.ID)
79 | val TWITTER_USER_PKEY : UniqueKey[TwitterUserRecord] = AbstractKeys.createUniqueKey(TwitterUser.TWITTER_USER, TwitterUser.TWITTER_USER.ID)
80 | val USER_PKEY : UniqueKey[UserRecord] = AbstractKeys.createUniqueKey(User.USER, User.USER.ID)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/CH08/app/generated/Public.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 |
13 | import java.util.ArrayList
14 | import java.util.Arrays
15 | import java.util.List
16 |
17 | import javax.annotation.Generated
18 |
19 | import org.jooq.Sequence
20 | import org.jooq.Table
21 | import org.jooq.impl.SchemaImpl
22 |
23 |
24 | object Public {
25 |
26 | /**
27 | * The reference instance of public
28 | */
29 | val PUBLIC = new Public
30 | }
31 |
32 | /**
33 | * This class is generated by jOOQ.
34 | */
35 | @Generated(
36 | value = Array(
37 | "http://www.jooq.org",
38 | "jOOQ version:3.7.0"
39 | ),
40 | comments = "This class is generated by jOOQ"
41 | )
42 | class Public extends SchemaImpl("public") {
43 |
44 | override def getSequences : List[Sequence[_]] = {
45 | val result = new ArrayList[Sequence[_]]
46 | result.addAll(getSequences0)
47 | result
48 | }
49 |
50 | private def getSequences0() : List[Sequence[_]] = {
51 | return Arrays.asList[Sequence[_]](
52 | Sequences.MENTION_SUBSCRIPTIONS_ID_SEQ,
53 | Sequences.MENTIONS_ID_SEQ,
54 | Sequences.TWITTER_USER_ID_SEQ,
55 | Sequences.USER_ID_SEQ)
56 | }
57 |
58 | override def getTables : List[Table[_]] = {
59 | val result = new ArrayList[Table[_]]
60 | result.addAll(getTables0)
61 | result
62 | }
63 |
64 | private def getTables0() : List[Table[_]] = {
65 | return Arrays.asList[Table[_]](
66 | MentionSubscriptions.MENTION_SUBSCRIPTIONS,
67 | Mentions.MENTIONS,
68 | PlayEvolutions.PLAY_EVOLUTIONS,
69 | TwitterUser.TWITTER_USER,
70 | User.USER)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/CH08/app/generated/Sequences.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import java.lang.Long
8 |
9 | import javax.annotation.Generated
10 |
11 | import org.jooq.Sequence
12 | import org.jooq.impl.SequenceImpl
13 |
14 |
15 | /**
16 | * Convenience access to all sequences in public
17 | */
18 | @Generated(
19 | value = Array(
20 | "http://www.jooq.org",
21 | "jOOQ version:3.7.0"
22 | ),
23 | comments = "This class is generated by jOOQ"
24 | )
25 | object Sequences {
26 |
27 | /**
28 | * The sequence public.mention_subscriptions_id_seq
29 | */
30 | val MENTION_SUBSCRIPTIONS_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("mention_subscriptions_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
31 |
32 | /**
33 | * The sequence public.mentions_id_seq
34 | */
35 | val MENTIONS_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("mentions_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
36 |
37 | /**
38 | * The sequence public.twitter_user_id_seq
39 | */
40 | val TWITTER_USER_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("twitter_user_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
41 |
42 | /**
43 | * The sequence public.user_id_seq
44 | */
45 | val USER_ID_SEQ : Sequence[Long] = new SequenceImpl[Long]("user_id_seq", Public.PUBLIC, org.jooq.impl.SQLDataType.BIGINT.nullable(false))
46 | }
47 |
--------------------------------------------------------------------------------
/CH08/app/generated/Tables.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 | import generated.tables.Mentions
9 | import generated.tables.PlayEvolutions
10 | import generated.tables.TwitterUser
11 | import generated.tables.User
12 |
13 | import javax.annotation.Generated
14 |
15 |
16 | /**
17 | * Convenience access to all tables in public
18 | */
19 | @Generated(
20 | value = Array(
21 | "http://www.jooq.org",
22 | "jOOQ version:3.7.0"
23 | ),
24 | comments = "This class is generated by jOOQ"
25 | )
26 | object Tables {
27 |
28 | /**
29 | * The table public.mention_subscriptions
30 | */
31 | val MENTION_SUBSCRIPTIONS = generated.tables.MentionSubscriptions.MENTION_SUBSCRIPTIONS
32 |
33 | /**
34 | * The table public.mentions
35 | */
36 | val MENTIONS = generated.tables.Mentions.MENTIONS
37 |
38 | /**
39 | * The table public.play_evolutions
40 | */
41 | val PLAY_EVOLUTIONS = generated.tables.PlayEvolutions.PLAY_EVOLUTIONS
42 |
43 | /**
44 | * The table public.twitter_user
45 | */
46 | val TWITTER_USER = generated.tables.TwitterUser.TWITTER_USER
47 |
48 | /**
49 | * The table public.user
50 | */
51 | val USER = generated.tables.User.USER
52 | }
53 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/MentionSubscriptions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.MentionSubscriptionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object MentionSubscriptions {
29 |
30 | /**
31 | * The reference instance of public.mention_subscriptions
32 | */
33 | val MENTION_SUBSCRIPTIONS = new MentionSubscriptions
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class MentionSubscriptions(alias : String, aliased : Table[MentionSubscriptionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[MentionSubscriptionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[MentionSubscriptionsRecord] = {
52 | classOf[MentionSubscriptionsRecord]
53 | }
54 |
55 | /**
56 | * The column public.mention_subscriptions.id
.
57 | */
58 | val ID : TableField[MentionSubscriptionsRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.mention_subscriptions.created_on
.
62 | */
63 | val CREATED_ON : TableField[MentionSubscriptionsRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
64 |
65 | /**
66 | * The column public.mention_subscriptions.user_id
.
67 | */
68 | val USER_ID : TableField[MentionSubscriptionsRecord, Long] = createField("user_id", org.jooq.impl.SQLDataType.BIGINT.nullable(false), "")
69 |
70 | /**
71 | * Create a public.mention_subscriptions
table reference
72 | */
73 | def this() = {
74 | this("mention_subscriptions", null, null)
75 | }
76 |
77 | /**
78 | * Create an aliased public.mention_subscriptions
table reference
79 | */
80 | def this(alias : String) = {
81 | this(alias, generated.tables.MentionSubscriptions.MENTION_SUBSCRIPTIONS, null)
82 | }
83 |
84 | private def this(alias : String, aliased : Table[MentionSubscriptionsRecord]) = {
85 | this(alias, aliased, null)
86 | }
87 |
88 | override def getIdentity : Identity[MentionSubscriptionsRecord, Long] = {
89 | Keys.IDENTITY_MENTION_SUBSCRIPTIONS
90 | }
91 |
92 | override def getPrimaryKey : UniqueKey[MentionSubscriptionsRecord] = {
93 | Keys.MENTION_SUBSCRIPTIONS_PKEY
94 | }
95 |
96 | override def getKeys : List[ UniqueKey[MentionSubscriptionsRecord] ] = {
97 | return Arrays.asList[ UniqueKey[MentionSubscriptionsRecord] ](Keys.MENTION_SUBSCRIPTIONS_PKEY)
98 | }
99 |
100 | override def as(alias : String) : MentionSubscriptions = {
101 | new MentionSubscriptions(alias, this)
102 | }
103 |
104 | /**
105 | * Rename this table
106 | */
107 | def rename(name : String) : MentionSubscriptions = {
108 | new MentionSubscriptions(name, null)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/Mentions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.MentionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object Mentions {
29 |
30 | /**
31 | * The reference instance of public.mentions
32 | */
33 | val MENTIONS = new Mentions
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class Mentions(alias : String, aliased : Table[MentionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[MentionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[MentionsRecord] = {
52 | classOf[MentionsRecord]
53 | }
54 |
55 | /**
56 | * The column public.mentions.id
.
57 | */
58 | val ID : TableField[MentionsRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.mentions.tweet_id
.
62 | */
63 | val TWEET_ID : TableField[MentionsRecord, String] = createField("tweet_id", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
64 |
65 | /**
66 | * The column public.mentions.user_id
.
67 | */
68 | val USER_ID : TableField[MentionsRecord, Long] = createField("user_id", org.jooq.impl.SQLDataType.BIGINT.nullable(false), "")
69 |
70 | /**
71 | * The column public.mentions.created_on
.
72 | */
73 | val CREATED_ON : TableField[MentionsRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
74 |
75 | /**
76 | * The column public.mentions.author_user_name
.
77 | */
78 | val AUTHOR_USER_NAME : TableField[MentionsRecord, String] = createField("author_user_name", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
79 |
80 | /**
81 | * The column public.mentions.text
.
82 | */
83 | val TEXT : TableField[MentionsRecord, String] = createField("text", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
84 |
85 | /**
86 | * Create a public.mentions
table reference
87 | */
88 | def this() = {
89 | this("mentions", null, null)
90 | }
91 |
92 | /**
93 | * Create an aliased public.mentions
table reference
94 | */
95 | def this(alias : String) = {
96 | this(alias, generated.tables.Mentions.MENTIONS, null)
97 | }
98 |
99 | private def this(alias : String, aliased : Table[MentionsRecord]) = {
100 | this(alias, aliased, null)
101 | }
102 |
103 | override def getIdentity : Identity[MentionsRecord, Long] = {
104 | Keys.IDENTITY_MENTIONS
105 | }
106 |
107 | override def getPrimaryKey : UniqueKey[MentionsRecord] = {
108 | Keys.MENTIONS_PKEY
109 | }
110 |
111 | override def getKeys : List[ UniqueKey[MentionsRecord] ] = {
112 | return Arrays.asList[ UniqueKey[MentionsRecord] ](Keys.MENTIONS_PKEY)
113 | }
114 |
115 | override def as(alias : String) : Mentions = {
116 | new Mentions(alias, this)
117 | }
118 |
119 | /**
120 | * Rename this table
121 | */
122 | def rename(name : String) : Mentions = {
123 | new Mentions(name, null)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/PlayEvolutions.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.PlayEvolutionsRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Integer
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Table
22 | import org.jooq.TableField
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.TableImpl
25 |
26 |
27 | object PlayEvolutions {
28 |
29 | /**
30 | * The reference instance of public.play_evolutions
31 | */
32 | val PLAY_EVOLUTIONS = new PlayEvolutions
33 | }
34 |
35 | /**
36 | * This class is generated by jOOQ.
37 | */
38 | @Generated(
39 | value = Array(
40 | "http://www.jooq.org",
41 | "jOOQ version:3.7.0"
42 | ),
43 | comments = "This class is generated by jOOQ"
44 | )
45 | class PlayEvolutions(alias : String, aliased : Table[PlayEvolutionsRecord], parameters : Array[ Field[_] ]) extends TableImpl[PlayEvolutionsRecord](alias, Public.PUBLIC, aliased, parameters, "") {
46 |
47 | /**
48 | * The class holding records for this type
49 | */
50 | override def getRecordType : Class[PlayEvolutionsRecord] = {
51 | classOf[PlayEvolutionsRecord]
52 | }
53 |
54 | /**
55 | * The column public.play_evolutions.id
.
56 | */
57 | val ID : TableField[PlayEvolutionsRecord, Integer] = createField("id", org.jooq.impl.SQLDataType.INTEGER.nullable(false), "")
58 |
59 | /**
60 | * The column public.play_evolutions.hash
.
61 | */
62 | val HASH : TableField[PlayEvolutionsRecord, String] = createField("hash", org.jooq.impl.SQLDataType.VARCHAR.length(255).nullable(false), "")
63 |
64 | /**
65 | * The column public.play_evolutions.applied_at
.
66 | */
67 | val APPLIED_AT : TableField[PlayEvolutionsRecord, Timestamp] = createField("applied_at", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
68 |
69 | /**
70 | * The column public.play_evolutions.apply_script
.
71 | */
72 | val APPLY_SCRIPT : TableField[PlayEvolutionsRecord, String] = createField("apply_script", org.jooq.impl.SQLDataType.CLOB, "")
73 |
74 | /**
75 | * The column public.play_evolutions.revert_script
.
76 | */
77 | val REVERT_SCRIPT : TableField[PlayEvolutionsRecord, String] = createField("revert_script", org.jooq.impl.SQLDataType.CLOB, "")
78 |
79 | /**
80 | * The column public.play_evolutions.state
.
81 | */
82 | val STATE : TableField[PlayEvolutionsRecord, String] = createField("state", org.jooq.impl.SQLDataType.VARCHAR.length(255), "")
83 |
84 | /**
85 | * The column public.play_evolutions.last_problem
.
86 | */
87 | val LAST_PROBLEM : TableField[PlayEvolutionsRecord, String] = createField("last_problem", org.jooq.impl.SQLDataType.CLOB, "")
88 |
89 | /**
90 | * Create a public.play_evolutions
table reference
91 | */
92 | def this() = {
93 | this("play_evolutions", null, null)
94 | }
95 |
96 | /**
97 | * Create an aliased public.play_evolutions
table reference
98 | */
99 | def this(alias : String) = {
100 | this(alias, generated.tables.PlayEvolutions.PLAY_EVOLUTIONS, null)
101 | }
102 |
103 | private def this(alias : String, aliased : Table[PlayEvolutionsRecord]) = {
104 | this(alias, aliased, null)
105 | }
106 |
107 | override def getPrimaryKey : UniqueKey[PlayEvolutionsRecord] = {
108 | Keys.PLAY_EVOLUTIONS_PKEY
109 | }
110 |
111 | override def getKeys : List[ UniqueKey[PlayEvolutionsRecord] ] = {
112 | return Arrays.asList[ UniqueKey[PlayEvolutionsRecord] ](Keys.PLAY_EVOLUTIONS_PKEY)
113 | }
114 |
115 | override def as(alias : String) : PlayEvolutions = {
116 | new PlayEvolutions(alias, this)
117 | }
118 |
119 | /**
120 | * Rename this table
121 | */
122 | def rename(name : String) : PlayEvolutions = {
123 | new PlayEvolutions(name, null)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/TwitterUser.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.TwitterUserRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.sql.Timestamp
15 | import java.util.Arrays
16 | import java.util.List
17 |
18 | import javax.annotation.Generated
19 |
20 | import org.jooq.Field
21 | import org.jooq.Identity
22 | import org.jooq.Table
23 | import org.jooq.TableField
24 | import org.jooq.UniqueKey
25 | import org.jooq.impl.TableImpl
26 |
27 |
28 | object TwitterUser {
29 |
30 | /**
31 | * The reference instance of public.twitter_user
32 | */
33 | val TWITTER_USER = new TwitterUser
34 | }
35 |
36 | /**
37 | * This class is generated by jOOQ.
38 | */
39 | @Generated(
40 | value = Array(
41 | "http://www.jooq.org",
42 | "jOOQ version:3.7.0"
43 | ),
44 | comments = "This class is generated by jOOQ"
45 | )
46 | class TwitterUser(alias : String, aliased : Table[TwitterUserRecord], parameters : Array[ Field[_] ]) extends TableImpl[TwitterUserRecord](alias, Public.PUBLIC, aliased, parameters, "") {
47 |
48 | /**
49 | * The class holding records for this type
50 | */
51 | override def getRecordType : Class[TwitterUserRecord] = {
52 | classOf[TwitterUserRecord]
53 | }
54 |
55 | /**
56 | * The column public.twitter_user.id
.
57 | */
58 | val ID : TableField[TwitterUserRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
59 |
60 | /**
61 | * The column public.twitter_user.created_on
.
62 | */
63 | val CREATED_ON : TableField[TwitterUserRecord, Timestamp] = createField("created_on", org.jooq.impl.SQLDataType.TIMESTAMP.nullable(false), "")
64 |
65 | /**
66 | * The column public.twitter_user.phone_number
.
67 | */
68 | val PHONE_NUMBER : TableField[TwitterUserRecord, String] = createField("phone_number", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
69 |
70 | /**
71 | * The column public.twitter_user.twitter_user_name
.
72 | */
73 | val TWITTER_USER_NAME : TableField[TwitterUserRecord, String] = createField("twitter_user_name", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
74 |
75 | /**
76 | * Create a public.twitter_user
table reference
77 | */
78 | def this() = {
79 | this("twitter_user", null, null)
80 | }
81 |
82 | /**
83 | * Create an aliased public.twitter_user
table reference
84 | */
85 | def this(alias : String) = {
86 | this(alias, generated.tables.TwitterUser.TWITTER_USER, null)
87 | }
88 |
89 | private def this(alias : String, aliased : Table[TwitterUserRecord]) = {
90 | this(alias, aliased, null)
91 | }
92 |
93 | override def getIdentity : Identity[TwitterUserRecord, Long] = {
94 | Keys.IDENTITY_TWITTER_USER
95 | }
96 |
97 | override def getPrimaryKey : UniqueKey[TwitterUserRecord] = {
98 | Keys.TWITTER_USER_PKEY
99 | }
100 |
101 | override def getKeys : List[ UniqueKey[TwitterUserRecord] ] = {
102 | return Arrays.asList[ UniqueKey[TwitterUserRecord] ](Keys.TWITTER_USER_PKEY)
103 | }
104 |
105 | override def as(alias : String) : TwitterUser = {
106 | new TwitterUser(alias, this)
107 | }
108 |
109 | /**
110 | * Rename this table
111 | */
112 | def rename(name : String) : TwitterUser = {
113 | new TwitterUser(name, null)
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/User.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables
5 |
6 |
7 | import generated.Keys
8 | import generated.Public
9 | import generated.tables.records.UserRecord
10 |
11 | import java.lang.Class
12 | import java.lang.Long
13 | import java.lang.String
14 | import java.util.Arrays
15 | import java.util.List
16 |
17 | import javax.annotation.Generated
18 |
19 | import org.jooq.Field
20 | import org.jooq.Identity
21 | import org.jooq.Table
22 | import org.jooq.TableField
23 | import org.jooq.UniqueKey
24 | import org.jooq.impl.TableImpl
25 |
26 |
27 | object User {
28 |
29 | /**
30 | * The reference instance of public.user
31 | */
32 | val USER = new User
33 | }
34 |
35 | /**
36 | * This class is generated by jOOQ.
37 | */
38 | @Generated(
39 | value = Array(
40 | "http://www.jooq.org",
41 | "jOOQ version:3.7.0"
42 | ),
43 | comments = "This class is generated by jOOQ"
44 | )
45 | class User(alias : String, aliased : Table[UserRecord], parameters : Array[ Field[_] ]) extends TableImpl[UserRecord](alias, Public.PUBLIC, aliased, parameters, "") {
46 |
47 | /**
48 | * The class holding records for this type
49 | */
50 | override def getRecordType : Class[UserRecord] = {
51 | classOf[UserRecord]
52 | }
53 |
54 | /**
55 | * The column public.user.id
.
56 | */
57 | val ID : TableField[UserRecord, Long] = createField("id", org.jooq.impl.SQLDataType.BIGINT.nullable(false).defaulted(true), "")
58 |
59 | /**
60 | * The column public.user.email
.
61 | */
62 | val EMAIL : TableField[UserRecord, String] = createField("email", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
63 |
64 | /**
65 | * The column public.user.password
.
66 | */
67 | val PASSWORD : TableField[UserRecord, String] = createField("password", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
68 |
69 | /**
70 | * The column public.user.firstname
.
71 | */
72 | val FIRSTNAME : TableField[UserRecord, String] = createField("firstname", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
73 |
74 | /**
75 | * The column public.user.lastname
.
76 | */
77 | val LASTNAME : TableField[UserRecord, String] = createField("lastname", org.jooq.impl.SQLDataType.VARCHAR.nullable(false), "")
78 |
79 | /**
80 | * Create a public.user
table reference
81 | */
82 | def this() = {
83 | this("user", null, null)
84 | }
85 |
86 | /**
87 | * Create an aliased public.user
table reference
88 | */
89 | def this(alias : String) = {
90 | this(alias, generated.tables.User.USER, null)
91 | }
92 |
93 | private def this(alias : String, aliased : Table[UserRecord]) = {
94 | this(alias, aliased, null)
95 | }
96 |
97 | override def getIdentity : Identity[UserRecord, Long] = {
98 | Keys.IDENTITY_USER
99 | }
100 |
101 | override def getPrimaryKey : UniqueKey[UserRecord] = {
102 | Keys.USER_PKEY
103 | }
104 |
105 | override def getKeys : List[ UniqueKey[UserRecord] ] = {
106 | return Arrays.asList[ UniqueKey[UserRecord] ](Keys.USER_PKEY)
107 | }
108 |
109 | override def as(alias : String) : User = {
110 | new User(alias, this)
111 | }
112 |
113 | /**
114 | * Rename this table
115 | */
116 | def rename(name : String) : User = {
117 | new User(name, null)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/CH08/app/generated/tables/records/MentionSubscriptionsRecord.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * This class is generated by jOOQ
3 | */
4 | package generated.tables.records
5 |
6 |
7 | import generated.tables.MentionSubscriptions
8 |
9 | import java.lang.Long
10 | import java.sql.Timestamp
11 |
12 | import javax.annotation.Generated
13 |
14 | import org.jooq.Field
15 | import org.jooq.Record1
16 | import org.jooq.Record3
17 | import org.jooq.Row3
18 | import org.jooq.impl.UpdatableRecordImpl
19 |
20 |
21 | /**
22 | * This class is generated by jOOQ.
23 | */
24 | @Generated(
25 | value = Array(
26 | "http://www.jooq.org",
27 | "jOOQ version:3.7.0"
28 | ),
29 | comments = "This class is generated by jOOQ"
30 | )
31 | class MentionSubscriptionsRecord extends UpdatableRecordImpl[MentionSubscriptionsRecord](MentionSubscriptions.MENTION_SUBSCRIPTIONS) with Record3[Long, Timestamp, Long] {
32 |
33 | /**
34 | * Setter for public.mention_subscriptions.id
.
35 | */
36 | def setId(value : Long) : Unit = {
37 | setValue(0, value)
38 | }
39 |
40 | /**
41 | * Getter for public.mention_subscriptions.id
.
42 | */
43 | def getId : Long = {
44 | val r = getValue(0)
45 | if (r == null) null else r.asInstanceOf[Long]
46 | }
47 |
48 | /**
49 | * Setter for public.mention_subscriptions.created_on
.
50 | */
51 | def setCreatedOn(value : Timestamp) : Unit = {
52 | setValue(1, value)
53 | }
54 |
55 | /**
56 | * Getter for public.mention_subscriptions.created_on
.
57 | */
58 | def getCreatedOn : Timestamp = {
59 | val r = getValue(1)
60 | if (r == null) null else r.asInstanceOf[Timestamp]
61 | }
62 |
63 | /**
64 | * Setter for public.mention_subscriptions.user_id
.
65 | */
66 | def setUserId(value : Long) : Unit = {
67 | setValue(2, value)
68 | }
69 |
70 | /**
71 | * Getter for public.mention_subscriptions.user_id
.
72 | */
73 | def getUserId : Long = {
74 | val r = getValue(2)
75 | if (r == null) null else r.asInstanceOf[Long]
76 | }
77 |
78 | // -------------------------------------------------------------------------
79 | // Primary key information
80 | // -------------------------------------------------------------------------
81 | override def key() : Record1[Long] = {
82 | return super.key.asInstanceOf[ Record1[Long] ]
83 | }
84 |
85 | // -------------------------------------------------------------------------
86 | // Record3 type implementation
87 | // -------------------------------------------------------------------------
88 |
89 | override def fieldsRow : Row3[Long, Timestamp, Long] = {
90 | super.fieldsRow.asInstanceOf[ Row3[Long, Timestamp, Long] ]
91 | }
92 |
93 | override def valuesRow : Row3[Long, Timestamp, Long] = {
94 | super.valuesRow.asInstanceOf[ Row3[Long, Timestamp, Long] ]
95 | }
96 | override def field1 : Field[Long] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.ID
97 | override def field2 : Field[Timestamp] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.CREATED_ON
98 | override def field3 : Field[Long] = MentionSubscriptions.MENTION_SUBSCRIPTIONS.USER_ID
99 | override def value1 : Long = getId
100 | override def value2 : Timestamp = getCreatedOn
101 | override def value3 : Long = getUserId
102 |
103 | override def value1(value : Long) : MentionSubscriptionsRecord = {
104 | setId(value)
105 | this
106 | }
107 |
108 | override def value2(value : Timestamp) : MentionSubscriptionsRecord = {
109 | setCreatedOn(value)
110 | this
111 | }
112 |
113 | override def value3(value : Long) : MentionSubscriptionsRecord = {
114 | setUserId(value)
115 | this
116 | }
117 |
118 | override def values(value1 : Long, value2 : Timestamp, value3 : Long) : MentionSubscriptionsRecord = {
119 | this.value1(value1)
120 | this.value2(value2)
121 | this.value3(value3)
122 | this
123 | }
124 |
125 | /**
126 | * Create a detached, initialised MentionSubscriptionsRecord
127 | */
128 | def this(id : Long, createdOn : Timestamp, userId : Long) = {
129 | this()
130 |
131 | setValue(0, id)
132 | setValue(1, createdOn)
133 | setValue(2, userId)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/CH08/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @main("Twitter SMS service dashboard") {
2 |
3 |
4 | }
5 |
--------------------------------------------------------------------------------
/CH08/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | @title
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
42 | @content
43 | @playscalajs.html.scripts("client")
44 |
45 |
46 |
--------------------------------------------------------------------------------
/CH08/build.sbt:
--------------------------------------------------------------------------------
1 | lazy val scalaV = "2.11.6"
2 |
3 | lazy val `ch08` = (project in file(".")).settings(
4 | scalaVersion := scalaV,
5 | scalaJSProjects := Seq(client),
6 | pipelineStages := Seq(scalaJSProd),
7 | libraryDependencies ++= Seq(
8 | "com.vmunier" %% "play-scalajs-scripts" % "0.2.2",
9 | "org.webjars" %% "webjars-play" % "2.4.0",
10 | "org.webjars.bower" % "angular-chart.js" % "0.7.1",
11 | "org.webjars.bower" % "angular-growl-2" % "0.7.4",
12 | jdbc,
13 | "org.postgresql" % "postgresql" % "9.4-1201-jdbc41",
14 | "org.jooq" % "jooq" % "3.7.0",
15 | "org.jooq" % "jooq-codegen-maven" % "3.7.0",
16 | "org.jooq" % "jooq-meta" % "3.7.0"
17 | ),
18 | WebKeys.importDirectly := true
19 | ).enablePlugins(PlayScala).dependsOn(client).aggregate(client)
20 |
21 | lazy val client = (project in file("modules/client")).settings(
22 | scalaVersion := scalaV,
23 | persistLauncher := true,
24 | persistLauncher in Test := false,
25 | scalaJSStage in Global := FastOptStage,
26 | libraryDependencies ++= Seq(
27 | "org.scala-js" %%% "scalajs-dom" % "0.8.0",
28 | "biz.enef" %%% "scalajs-angulate" % "0.2",
29 | "com.lihaoyi" %%% "utest" % "0.3.1" % "test"
30 | ),
31 | jsDependencies ++= Seq(
32 | "org.webjars.bower" % "angular" % "1.4.0" / "angular.min.js",
33 | "org.webjars.bower" % "angular-route" % "1.4.0" / "angular-route.min.js" dependsOn "angular.min.js",
34 | "org.webjars.bower" % "angular-websocket" % "1.0.13" / "dist/angular-websocket.min.js" dependsOn "angular.min.js",
35 | "org.webjars.bower" % "Chart.js" % "1.0.2" / "Chart.min.js",
36 | "org.webjars.bower" % "angular-chart.js" % "0.7.1" / "dist/angular-chart.js" dependsOn "Chart.min.js",
37 | "org.webjars.bower" % "angular-growl-2" % "0.7.4" / "build/angular-growl.min.js",
38 | RuntimeDOM % "test"
39 | ),
40 | skip in packageJSDependencies := false,
41 | testFrameworks += new TestFramework("utest.runner.Framework")
42 | ).enablePlugins(ScalaJSPlugin, ScalaJSPlay, SbtWeb)
43 |
44 | val generateJOOQ = taskKey[Seq[File]]("Generate JooQ classes")
45 |
46 | val generateJOOQTask = (baseDirectory, fullClasspath in Compile, runner in Compile, streams) map { (base, cp, r, s) =>
47 | toError(r.run("org.jooq.util.GenerationTool", cp.files, Array("conf/chapter7.xml"), s.log))
48 | ((base / "app" / "generated") ** "*.scala").get
49 | }
50 |
51 | generateJOOQ <<= generateJOOQTask
--------------------------------------------------------------------------------
/CH08/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.crypto.secret="U3spB5^7L;EK_:<=OKxwpOJGM:0JsAyTu2pWhKsqbiGMO@tT_8voDNs:x7q=x?e<"
2 |
3 | play.i18n.langs= ["en"]
4 |
5 | db.default.driver="org.postgresql.Driver"
6 | db.default.url="jdbc:postgresql://localhost/chapter7"
7 | db.default.user=user
8 | db.default.password=secret
9 | db.default.maximumPoolSize = 9
--------------------------------------------------------------------------------
/CH08/conf/chapter7.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | org.postgresql.Driver
5 | jdbc:postgresql://localhost/chapter7
6 | user
7 | secret
8 |
9 |
10 | org.jooq.util.ScalaGenerator
11 |
12 | org.jooq.util.postgres.PostgresDatabase
13 | public
14 | .*
15 |
16 |
17 |
18 | generated
19 | app
20 |
21 |
22 |
--------------------------------------------------------------------------------
/CH08/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %logger{15} - %message%n%xException{5}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CH08/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 | GET /graphs controllers.Application.graphs
8 |
9 | # Map static resources from the /public folder to the /assets URL path
10 | GET /webjars/*file controllers.WebJarAssets.at(file)
11 | GET /assets/*file controllers.Assets.at(path="/public", file)
12 |
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/public/partials/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/scala/dashboard/DashboardApp.scala:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import biz.enef.angulate.ext.{Route, RouteProvider}
4 | import biz.enef.angulate._
5 | import scala.scalajs.js.JSApp
6 |
7 | object DashboardApp extends JSApp {
8 |
9 | def main(): Unit = {
10 | val module = angular.createModule("dashboard", Seq("ngRoute", "ngWebSocket", "chart.js", "angular-growl"))
11 |
12 | module.serviceOf[GraphDataService]
13 |
14 | module.controllerOf[DashboardCtrl]
15 |
16 | module.config { ($routeProvider: RouteProvider) =>
17 | $routeProvider
18 | .when("/dashboard", Route(templateUrl = "/assets/partials/dashboard.html", controller = "dashboard.DashboardCtrl"))
19 | .otherwise(Route(redirectTo = "/dashboard"))
20 | }
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/scala/dashboard/DashboardCtrl.scala:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import biz.enef.angulate._
4 | import org.scalajs.dom._
5 | import scala.scalajs.js
6 | import scalajs.js.Dynamic
7 |
8 | class DashboardCtrl($scope: Dynamic, graphDataService: GraphDataService) extends ScopeController {
9 | graphDataService.fetchGraph(GraphType.MonthlySubscriptions, { (graphData: js.Dynamic) =>
10 | console.log(graphData)
11 | $scope.monthlySubscriptions = graphData
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/scala/dashboard/GraphDataService.scala:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import biz.enef.angulate._
4 | import org.scalajs.dom._
5 | import scala.scalajs.js.{Dynamic, JSON}
6 | import scala.collection._
7 |
8 | class GraphDataService($websocket: WebsocketService, growl: GrowlService) extends Service {
9 | val dataStream = $websocket("ws://localhost:9000/graphs", Dynamic.literal("reconnectIfNotNormalClose" -> true))
10 |
11 | private val callbacks =
12 | mutable.Map.empty[GraphType.Value, Dynamic => Unit]
13 |
14 | def fetchGraph(graphType: GraphType.Value, callback: Dynamic => Unit) = {
15 | callbacks += graphType -> callback
16 | dataStream.send(graphType.toString)
17 | }
18 |
19 | dataStream.onMessage { (event: MessageEvent) =>
20 | val json: Dynamic = JSON.parse(event.data.toString)
21 | val graphType = GraphType.withName(json.graph_type.toString)
22 | callbacks.get(graphType).map { callback =>
23 | callback(json)
24 | } getOrElse {
25 | console.log(s"Unknown graph type $graphType")
26 | }
27 | }
28 |
29 | dataStream.onClose { (event: CloseEvent) =>
30 | growl.error(s"Server connection closed, attempting to reconnect")
31 | }
32 |
33 | dataStream.onOpen { (event: Dynamic) =>
34 | growl.info("Server connection established")
35 | }
36 | }
37 |
38 | object GraphType extends Enumeration {
39 | val MonthlySubscriptions = Value
40 | }
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/scala/dashboard/GrowlService.scala:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import biz.enef.angulate.core.ProvidedService
4 |
5 | import scala.scalajs.js
6 |
7 | trait GrowlService extends ProvidedService {
8 | def info(message: String): Unit = js.native
9 | def warning(message: String): Unit = js.native
10 | def success(message: String): Unit = js.native
11 | def error(message: String): Unit = js.native
12 | }
13 |
--------------------------------------------------------------------------------
/CH08/modules/client/src/main/scala/dashboard/WebsocketService.scala:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import biz.enef.angulate.core.{HttpPromise, ProvidedService}
4 | import org.scalajs.dom._
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.UndefOr
8 |
9 | trait WebsocketService extends ProvidedService {
10 | def apply(url: String, options: UndefOr[Dynamic] = js.undefined): WebsocketDataStream = js.native
11 | }
12 |
13 | trait WebsocketDataStream extends js.Object {
14 | def send[T](data: js.Any): HttpPromise[T] = js.native
15 | def onMessage(callback: js.Function1[MessageEvent, Unit], options: UndefOr[js.Dynamic] = js.undefined): Unit = js.native
16 | def onClose(callback: js.Function1[CloseEvent, Unit]): Unit = js.native
17 | def onOpen(callback: js.Function1[js.Dynamic, Unit]): Unit = js.native
18 | }
--------------------------------------------------------------------------------
/CH08/modules/client/src/test/scala/services/GraphDataServiceSuite.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import biz.enef.angulate.core.HttpPromise
4 | import dashboard._
5 | import org.scalajs.dom._
6 | import utest._
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.UndefOr
10 | import scala.scalajs.js.annotation.JSExportAll
11 |
12 | object GraphDataServiceSuite extends TestSuite {
13 | val tests = TestSuite {
14 | "GraphDataService should establish a WebSocket connection at startup" - {
15 | val growlMock = new GrowlServiceMock
16 | val mockedWebsocketDataStream = new WebsocketDataStreamMock()
17 | val mockedWebsocketService: js.Function = {
18 | (url: String, options: js.UndefOr[js.Dynamic]) =>
19 | mockedWebsocketDataStream.asInstanceOf[WebsocketDataStream]
20 | }
21 |
22 | new GraphDataService(
23 | mockedWebsocketService.asInstanceOf[WebsocketService],
24 | growlMock.asInstanceOf[GrowlService]
25 | )
26 |
27 | assert(mockedWebsocketDataStream.isInitialized)
28 | }
29 | }
30 | }
31 |
32 | @JSExportAll
33 | class GrowlServiceMock
34 |
35 | @JSExportAll
36 | class WebsocketDataStreamMock {
37 | val isInitialized = true
38 | def send[T](data: js.Any): HttpPromise[T] = ???
39 | def onMessage(
40 | callback: js.Function1[MessageEvent, Unit],
41 | options: UndefOr[js.Dynamic] = js.undefined
42 | ): Unit = {}
43 | def onClose(callback: js.Function1[CloseEvent, Unit]): Unit = {}
44 | def onOpen(callback: js.Function1[js.Dynamic, Unit]): Unit = {}
45 | }
46 |
--------------------------------------------------------------------------------
/CH08/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // Comment to get more information during initialization
2 | logLevel := Level.Warn
3 |
4 | // Resolvers
5 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"
6 |
7 | // Sbt plugins
8 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
9 |
10 | addSbtPlugin("com.vmunier" % "sbt-play-scalajs" % "0.2.6")
11 |
12 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.3")
13 |
--------------------------------------------------------------------------------
/CH08/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH08/public/images/favicon.png
--------------------------------------------------------------------------------
/CH08/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 | body { padding-top: 70px; }
2 |
--------------------------------------------------------------------------------
/CH09/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | /out
11 | /.idea_modules
12 | /.classpath
13 | /.project
14 | /RUNNING_PID
15 | /.settings
16 | conf/twitter.conf
17 |
--------------------------------------------------------------------------------
/CH09/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.libs.iteratee._
6 | import play.api.libs.json._
7 | import play.api.mvc._
8 | import services.TwitterStreamService
9 |
10 | class Application @Inject() (twitterStream: TwitterStreamService)
11 | extends Controller {
12 |
13 | def index = Action { implicit request =>
14 | val parsedTopics = parseTopicsAndDigestRate(request.queryString)
15 | Ok(views.html.index(parsedTopics, request.rawQueryString))
16 | }
17 |
18 | def stream = WebSocket.using[JsValue] { request =>
19 | val parsedTopics = parseTopicsAndDigestRate(request.queryString)
20 | val out = twitterStream.stream(parsedTopics)
21 | val in: Iteratee[JsValue, Unit] = Iteratee.ignore[JsValue]
22 | (in, out)
23 | }
24 |
25 | private def parseTopicsAndDigestRate(
26 | queryString: Map[String, Seq[String]]
27 | ): Map[String, Int] = {
28 | val topics = queryString.getOrElse("topic", Seq.empty)
29 | topics.map { topicAndRate =>
30 | val Array(topic, digestRate) = topicAndRate.split(':')
31 | (topic, digestRate.toInt)
32 | }.toMap[String, Int]
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/CH09/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(topicsAndRate: Map[String, Int], queryString: String)(implicit request: RequestHeader)
2 |
3 |
4 |
5 |
6 | Reactive Tweets
7 |
8 |
9 |
10 |
11 | @if(topicsAndRate.nonEmpty) {
12 |
13 | @topicsAndRate.keys.map { topic =>
14 |
17 |
18 | }
19 |
20 |
44 | } else {
45 | No topics selected.
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/CH09/build.sbt:
--------------------------------------------------------------------------------
1 | name := """ch09"""
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 | lazy val root = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.7"
8 |
9 | libraryDependencies ++= Seq(
10 | ws,
11 | "com.ning" % "async-http-client" % "1.9.29",
12 | "com.typesafe.play.extras" %% "iteratees-extras" % "1.5.0",
13 | "com.typesafe.play" %% "play-streams-experimental" % "2.4.2",
14 | "com.typesafe.akka" % "akka-stream-experimental_2.11" % "1.0"
15 | )
16 |
17 | resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"
18 |
19 | resolvers += "Typesafe private" at "https://private-repo.typesafe.com/typesafe/maven-releases"
20 |
21 | routesGenerator := InjectedRoutesGenerator
22 |
--------------------------------------------------------------------------------
/CH09/conf/application.conf:
--------------------------------------------------------------------------------
1 | play.crypto.secret = "changeme"
2 |
3 | play.i18n.langs = [ "en" ]
4 |
5 | include "twitter.conf"
6 |
--------------------------------------------------------------------------------
/CH09/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CH09/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 | GET /stream controllers.Application.stream
8 |
9 | # Map static resources from the /public folder to the /assets URL path
10 | GET /assets/*file controllers.Assets.at(path="/public", file)
11 |
--------------------------------------------------------------------------------
/CH09/conf/twitter.conf:
--------------------------------------------------------------------------------
1 | # Twitter
2 | twitter.apiKey=""
3 | twitter.apiSecret=""
4 | twitter.accessToken=""
5 | twitter.accessTokenSecret=""
--------------------------------------------------------------------------------
/CH09/project/build.properties:
--------------------------------------------------------------------------------
1 | #Activator-generated Properties
2 | #Thu Jul 02 15:21:14 CEST 2015
3 | template.uuid=49a31253-85ed-4c4f-9041-f2f030591a8d
4 | sbt.version=0.13.9
5 |
--------------------------------------------------------------------------------
/CH09/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3")
3 |
4 | // web plugins
5 |
6 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
7 |
8 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.6")
9 |
10 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3")
11 |
12 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7")
13 |
14 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")
15 |
16 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0")
--------------------------------------------------------------------------------
/CH10/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | target
3 | /.idea
4 | /.idea_modules
5 | /.classpath
6 | /.project
7 | /.settings
8 | /RUNNING_PID
9 |
--------------------------------------------------------------------------------
/CH10/README:
--------------------------------------------------------------------------------
1 | This is your new Play application
2 | =================================
3 |
4 | This file will be packaged with your application, when using `activator dist`.
5 |
--------------------------------------------------------------------------------
/CH10/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | (function (requirejs) {
2 | 'use strict';
3 | requirejs.config({
4 | shim: {
5 | 'jsRoutes': {
6 | deps: [],
7 | exports: 'jsRoutes'
8 | }
9 | },
10 | paths: {
11 | 'jquery': ['../lib/jquery/jquery']
12 | }
13 | });
14 |
15 | requirejs.onError = function (err) {
16 | console.log(err);
17 | };
18 |
19 | require(['jquery'], function ($) {
20 | $(document).ready(function () {
21 | $('#button').on('click', function () {
22 | jsRoutes.controllers.Application.text().ajax({
23 | success: function (text) {
24 | $('#text').text(text);
25 | }, error: function () {
26 | alert('Uh oh');
27 | }
28 | });
29 | });
30 | });
31 | });
32 | })(requirejs);
33 |
--------------------------------------------------------------------------------
/CH10/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject._
4 | import play.api._
5 | import play.api.mvc._
6 |
7 | class Application @Inject() (configuration: Configuration) extends Controller {
8 |
9 | def index = Action { implicit request =>
10 | Ok(views.html.index("Hello"))
11 | }
12 |
13 | def text = Action {
14 | Ok(configuration.getString("text").getOrElse("Hello world"))
15 | }
16 |
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/CH10/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(message: String)(implicit request: RequestHeader)
2 | @main(message) {
3 |
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/CH10/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)(implicit request: RequestHeader)
2 |
3 |
4 |
5 |
6 |
7 | @title
8 |
9 |
10 |
12 | @helper.javascriptRouter("jsRoutes")(
13 | routes.javascript.Application.text
14 | )
15 |
16 |
17 | @content
18 |
19 |
20 |
--------------------------------------------------------------------------------
/CH10/build.sbt:
--------------------------------------------------------------------------------
1 | import com.typesafe.sbt.packager.archetypes.ServerLoader
2 |
3 | name := """ch10"""
4 |
5 | version := "1.0-SNAPSHOT"
6 |
7 | lazy val root = (project in file("."))
8 | .enablePlugins(
9 | PlayScala,
10 | DebianPlugin,
11 | JavaServerAppPackaging
12 | )
13 |
14 | scalaVersion := "2.11.7"
15 |
16 | libraryDependencies ++= Seq(
17 | "org.webjars" %% "webjars-play" % "2.4.0-1",
18 | "org.webjars" % "jquery" % "2.1.4",
19 | "org.seleniumhq.selenium" % "selenium-firefox-driver" % "2.53.0",
20 | "org.scalatest" %% "scalatest" % "2.2.1" % "test",
21 | "org.scalatestplus" %% "play" % "1.4.0-M4" % "test"
22 | )
23 |
24 | routesGenerator := InjectedRoutesGenerator
25 |
26 | pipelineStages := Seq(rjs)
27 |
28 | RjsKeys.mainModule := "application"
29 |
30 | RjsKeys.mainConfig := "application"
31 |
32 | maintainer := "Manuel Bernhardt "
33 |
34 | packageSummary in Linux := "Chapter 10 of Reactive Web Applications"
35 |
36 | packageDescription := "This package installs the Play Application used as an example in Chapter 10 of the book Reactive Web Applications (Manning)"
37 |
38 | serverLoading in Debian := ServerLoader.Systemd
39 |
40 | dockerExposedPorts in Docker := Seq(9000, 9443)
41 |
--------------------------------------------------------------------------------
/CH10/conf/application.conf:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for the application.
2 | # ~~~~~
3 |
4 | # Secret key
5 | # ~~~~~
6 | # The secret key is used to secure cryptographics functions.
7 | #
8 | # This must be changed for production, but we recommend not changing it in this file.
9 | #
10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details.
11 | play.crypto.secret = "changeme"
12 | play.crypto.secret = ${?APPLICATION_SECRET}
13 |
14 | # The application languages
15 | # ~~~~~
16 | play.i18n.langs = [ "en" ]
17 |
18 | # Router
19 | # ~~~~~
20 | # Define the Router object to use for this application.
21 | # This router will be looked up first when the application is starting up,
22 | # so make sure this is the entry point.
23 | # Furthermore, it's assumed your route file is named properly.
24 | # So for an application router like `my.application.Router`,
25 | # you may need to define a router file `conf/my.application.routes`.
26 | # Default to Routes in the root package (and conf/routes)
27 | # play.http.router = my.application.Routes
28 |
29 | # Database configuration
30 | # ~~~~~
31 | # You can declare as many datasources as you want.
32 | # By convention, the default datasource is named `default`
33 | #
34 | # db.default.driver=org.h2.Driver
35 | # db.default.url="jdbc:h2:mem:play"
36 | # db.default.username=sa
37 | # db.default.password=""
38 |
39 | # Evolutions
40 | # ~~~~~
41 | # You can disable evolutions if needed
42 | # play.evolutions.enabled=false
43 |
44 | # You can disable evolutions for a specific datasource if necessary
45 | # play.evolutions.db.default.enabled=false
46 |
47 |
48 | text="Hello from the configuration file"
49 | text=${?TEXT}
50 |
--------------------------------------------------------------------------------
/CH10/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CH10/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 | GET /text controllers.Application.text
8 |
9 | # Map static resources from the /public folder to the /assets URL path
10 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
11 | GET /webjars/*file controllers.WebJarAssets.at(file)
12 |
--------------------------------------------------------------------------------
/CH10/project/build.properties:
--------------------------------------------------------------------------------
1 | #Activator-generated Properties
2 | #Thu Jul 23 09:07:38 CEST 2015
3 | template.uuid=a91771f5-1745-4f51-b877-badeea610f64
4 | sbt.version=0.13.9
5 |
--------------------------------------------------------------------------------
/CH10/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.2")
3 |
4 | // web plugins
5 |
6 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
7 |
8 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.6")
9 |
10 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3")
11 |
12 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7")
13 |
14 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")
15 |
16 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0")
17 |
--------------------------------------------------------------------------------
/CH10/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH10/public/images/favicon.png
--------------------------------------------------------------------------------
/CH10/public/javascripts/hello.js:
--------------------------------------------------------------------------------
1 | if (window.console) {
2 | console.log("Welcome to your Play application's JavaScript!");
3 | }
--------------------------------------------------------------------------------
/CH10/public/stylesheets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH10/public/stylesheets/main.css
--------------------------------------------------------------------------------
/CH10/test/ApplicationSpec.scala:
--------------------------------------------------------------------------------
1 | import org.scalatestplus.play._
2 |
3 | class ApplicationSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite with FirefoxFactory {
4 |
5 | "The Application" must {
6 | "display a text when clicking on a button" in {
7 | go to (s"http://localhost:$port")
8 | pageTitle mustBe "Hello"
9 | click on find(id("button")).value
10 | eventually {
11 | find(id("text")).map(_.text) mustBe app.configuration.getString("text")
12 | }
13 | }
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/CH11/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | target
3 | /.idea
4 | /.idea_modules
5 | /.classpath
6 | /.project
7 | /.settings
8 | /RUNNING_PID
9 |
--------------------------------------------------------------------------------
/CH11/app/actors/RandomNumberComputer.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import actors.RandomNumberComputer.{RandomNumber, ComputeRandomNumber}
4 | import akka.actor.{Props, Actor}
5 | import scala.util.Random
6 |
7 | class RandomNumberComputer extends Actor {
8 | def receive = {
9 | case ComputeRandomNumber(max) =>
10 | sender() ! RandomNumber(Random.nextInt(max))
11 | }
12 | }
13 |
14 | object RandomNumberComputer {
15 | def props = Props[RandomNumberComputer]
16 | case class ComputeRandomNumber(max: Int)
17 | case class RandomNumber(n: Int)
18 | }
19 |
--------------------------------------------------------------------------------
/CH11/app/actors/RandomNumberFetcher.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import actors.RandomNumberFetcher.{RandomNumber, FetchRandomNumber}
4 | import akka.actor.{Props, Actor}
5 | import play.api.libs.json.{JsResultException, JsArray, Json}
6 | import play.api.libs.ws.WSClient
7 | import scala.concurrent.Future
8 | import scala.concurrent.duration._
9 | import akka.pattern.{CircuitBreakerOpenException, CircuitBreaker, pipe}
10 |
11 | import scala.util.Random
12 | import scala.util.control.NonFatal
13 |
14 | class RandomNumberFetcher(ws: WSClient) extends Actor {
15 | implicit val ec = context.dispatcher
16 |
17 | val breaker = CircuitBreaker(
18 | scheduler = context.system.scheduler,
19 | maxFailures = 5,
20 | callTimeout = 3.seconds,
21 | resetTimeout = 30.seconds
22 | )
23 |
24 | def receive = {
25 | case FetchRandomNumber(max) =>
26 | val safeCall = breaker.withCircuitBreaker(
27 | fetchRandomNumber(max).map(RandomNumber)
28 | )
29 | safeCall recover {
30 | case js: JsResultException => computeRandomNumber(max)
31 | case o: CircuitBreakerOpenException => computeRandomNumber(max)
32 | } pipeTo sender()
33 | }
34 |
35 | def computeRandomNumber(max: Int) = RandomNumber(Random.nextInt(max))
36 |
37 | def fetchRandomNumber(max: Int): Future[Int] =
38 | ws
39 | .url("https://api.random.org/json-rpc/1/invoke")
40 | .post(Json.obj(
41 | "jsonrpc" -> "2.0",
42 | "method" -> "generateIntegers",
43 | "params" -> Json.obj(
44 | "apiKey" -> "",
45 | "n" -> 1,
46 | "min" -> 0,
47 | "max" -> max,
48 | "replacement" -> true,
49 | "base" -> 10
50 | ),
51 | "id" -> 42
52 | )).map { response =>
53 | (response.json \ "result" \ "random" \ "data").as[JsArray].value.head.as[Int]
54 | }
55 | }
56 |
57 | object RandomNumberFetcher {
58 | def props(ws: WSClient) = Props(classOf[RandomNumberFetcher], ws)
59 | case class FetchRandomNumber(max: Int)
60 | case class RandomNumber(n: Int)
61 | }
62 |
--------------------------------------------------------------------------------
/CH11/app/controllers/Application.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject.Inject
4 |
5 | import actors.RandomNumberFetcher
6 | import actors.RandomNumberFetcher.{FetchRandomNumber, RandomNumber}
7 | import akka.actor.ActorSystem
8 | import akka.pattern.{AskTimeoutException, ask}
9 | import akka.util.Timeout
10 | import play.api.libs.ws.WSClient
11 | import play.api.mvc._
12 |
13 | import scala.concurrent.ExecutionContext
14 | import scala.concurrent.duration._
15 |
16 |
17 | class Application @Inject() (ws: WSClient,
18 | ec: ExecutionContext,
19 | system: ActorSystem) extends Controller {
20 |
21 | implicit val executionContext = ec
22 | implicit val timeout = Timeout(2000.millis)
23 |
24 | val fetcher = system.actorOf(RandomNumberFetcher.props(ws))
25 |
26 | def index = Action { implicit request =>
27 | Ok(views.html.index())
28 | }
29 |
30 | def compute = Action.async { implicit request =>
31 | (fetcher ? FetchRandomNumber(10)).map {
32 | case RandomNumber(r) =>
33 | Redirect(routes.Application.index())
34 | .flashing("result" -> s"The result is $r")
35 | case other =>
36 | InternalServerError
37 | } recover {
38 | case to: AskTimeoutException =>
39 | Ok("Sorry, we are overloaded")
40 | }
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/CH11/app/services/DiceService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import scala.concurrent.Future
4 |
5 | trait DiceService {
6 | def throwDice: Future[Int]
7 | }
8 | class RollingDiceService extends DiceService {
9 | override def throwDice: Future[Int] =
10 | Future.successful {
11 | 4 // chosen by fair dice roll.
12 | // guaranteed to be random.
13 | }
14 | }
--------------------------------------------------------------------------------
/CH11/app/services/RandomNumberService.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import scala.concurrent.Future
4 | import scala.util.control.NonFatal
5 | import scala.concurrent.ExecutionContext.Implicits._
6 |
7 | trait RandomNumberService {
8 | def generateRandomNumber: Future[Int]
9 | }
10 |
11 | class DiceDrivenRandomNumberService(dice: DiceService)
12 | extends RandomNumberService {
13 | override def generateRandomNumber: Future[Int] = dice.throwDice.recoverWith {
14 | case NonFatal(t) => generateRandomNumber
15 | }
16 | }
--------------------------------------------------------------------------------
/CH11/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @()(implicit flash: Flash)
2 |
3 | @main("Welcome") {
4 |
5 | @flash.get("result").map { result =>
6 | @result
7 | }
8 |
9 | @helper.form(routes.Application.compute()) {
10 |
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/CH11/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @(title: String)(content: Html)
2 |
3 |
4 |
5 |
6 |
7 | @title
8 |
9 |
10 |
11 |
12 |
13 | @content
14 |
15 |
16 |
--------------------------------------------------------------------------------
/CH11/build.sbt:
--------------------------------------------------------------------------------
1 | name := """CH11"""
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 | lazy val root = (project in file(".")).enablePlugins(PlayScala)
6 |
7 | scalaVersion := "2.11.7"
8 |
9 | libraryDependencies ++= Seq(
10 | ws,
11 | "org.scalatest" %% "scalatest" % "2.2.1" % Test,
12 | "org.scalatestplus" %% "play" % "1.4.0-M3" % Test,
13 | "com.typesafe.akka" %% "akka-testkit" % "2.3.11" % Test
14 | )
15 |
16 | testOptions in Test += Tests.Argument(
17 | "-F",
18 | sys.props.getOrElse("SCALING_FACTOR", default = "1.0")
19 | )
20 |
21 | routesGenerator := InjectedRoutesGenerator
22 |
--------------------------------------------------------------------------------
/CH11/conf/application.conf:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for the application.
2 | # ~~~~~
3 |
4 | # Secret key
5 | # ~~~~~
6 | # The secret key is used to secure cryptographics functions.
7 | #
8 | # This must be changed for production, but we recommend not changing it in this file.
9 | #
10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details.
11 | play.crypto.secret = "changeme"
12 |
13 | # The application languages
14 | # ~~~~~
15 | play.i18n.langs = [ "en" ]
16 |
17 | # Router
18 | # ~~~~~
19 | # Define the Router object to use for this application.
20 | # This router will be looked up first when the application is starting up,
21 | # so make sure this is the entry point.
22 | # Furthermore, it's assumed your route file is named properly.
23 | # So for an application router like `my.application.Router`,
24 | # you may need to define a router file `conf/my.application.routes`.
25 | # Default to Routes in the root package (and conf/routes)
26 | # play.http.router = my.application.Routes
27 |
28 | # Database configuration
29 | # ~~~~~
30 | # You can declare as many datasources as you want.
31 | # By convention, the default datasource is named `default`
32 | #
33 | # db.default.driver=org.h2.Driver
34 | # db.default.url="jdbc:h2:mem:play"
35 | # db.default.username=sa
36 | # db.default.password=""
37 |
38 | # Evolutions
39 | # ~~~~~
40 | # You can disable evolutions if needed
41 | # play.evolutions.enabled=false
42 |
43 | # You can disable evolutions for a specific datasource if necessary
44 | # play.evolutions.db.default.enabled=false
45 |
--------------------------------------------------------------------------------
/CH11/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel - %logger - %message%n%xException
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/CH11/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / controllers.Application.index
7 | GET /compute controllers.Application.compute
8 |
9 | # Map static resources from the /public folder to the /assets URL path
10 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
11 |
--------------------------------------------------------------------------------
/CH11/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.9
2 |
--------------------------------------------------------------------------------
/CH11/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.2")
3 |
4 | // web plugins
5 |
6 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
7 |
8 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.6")
9 |
10 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.3")
11 |
12 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.7")
13 |
14 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")
15 |
16 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0")
17 |
--------------------------------------------------------------------------------
/CH11/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH11/public/images/favicon.png
--------------------------------------------------------------------------------
/CH11/public/javascripts/hello.js:
--------------------------------------------------------------------------------
1 | if (window.console) {
2 | console.log("Welcome to your Play application's JavaScript!");
3 | }
--------------------------------------------------------------------------------
/CH11/public/stylesheets/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuelbernhardt/reactive-web-applications/b3b2df93f5fc998a6d61a17c553dbae349537970/CH11/public/stylesheets/main.css
--------------------------------------------------------------------------------
/CH11/test/actors/RandomNumberComputerSpec.scala:
--------------------------------------------------------------------------------
1 | package actors
2 |
3 | import akka.actor.SupervisorStrategy.Restart
4 | import akka.actor._
5 | import akka.testkit._
6 | import com.typesafe.config.ConfigFactory
7 | import scala.concurrent.duration._
8 | import org.scalatest._
9 | import actors.RandomNumberComputer._
10 |
11 | class RandomNumberComputerSpec(_system: ActorSystem)
12 | extends TestKit(_system)
13 | with ImplicitSender
14 | with FlatSpecLike
15 | with ShouldMatchers
16 | with BeforeAndAfterAll {
17 |
18 | def this() = this(
19 | ActorSystem(
20 | "RandomNumberComputerSpec",
21 | ConfigFactory.parseString(
22 | s"akka.test.timefactor=" +
23 | sys.props.getOrElse("SCALING_FACTOR", default = "1.0")
24 | )
25 | )
26 | )
27 |
28 | override def afterAll {
29 | TestKit.shutdownActorSystem(system)
30 | }
31 |
32 | "A RandomNumberComputerSpec" should "send back a random number" in {
33 | val randomNumberComputer = system.actorOf(RandomNumberComputer.props)
34 | within(100.milliseconds.dilated) {
35 | randomNumberComputer ! ComputeRandomNumber(100)
36 | expectMsgType[RandomNumber]
37 | }
38 | }
39 |
40 | it should "fail when the maximum is a negative number" in {
41 |
42 | class StepParent(target: ActorRef) extends Actor {
43 | override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() {
44 | case t: Throwable =>
45 | target ! t
46 | Restart
47 | }
48 | def receive = {
49 | case props: Props =>
50 | sender ! context.actorOf(props)
51 | }
52 | }
53 |
54 | val parent = system.actorOf(Props(new StepParent(testActor)), name = "stepParent")
55 | parent ! RandomNumberComputer.props
56 | val actorUnderTest = expectMsgType[ActorRef]
57 | actorUnderTest ! ComputeRandomNumber(-1)
58 | expectMsgType[IllegalArgumentException]
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/CH11/test/services/DiceDrivenRandomNumberServiceSpec.scala:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import java.util.concurrent.atomic.AtomicInteger
4 |
5 | import org.scalatest.time.{Millis, Span}
6 | import org.scalatest.{ShouldMatchers, FlatSpec}
7 | import org.scalatest.concurrent.ScalaFutures
8 | import scala.concurrent.Future
9 |
10 | class DiceDrivenRandomNumberServiceSpec
11 | extends FlatSpec
12 | with ScalaFutures
13 | with ShouldMatchers {
14 |
15 | "The DiceDrivenRandomNumberService" should
16 | "return a number provided by a dice" in {
17 |
18 | implicit val patienceConfig =
19 | PatienceConfig(
20 | timeout = scaled(Span(15, Millis)),
21 | interval = scaled(Span(15, Millis))
22 | )
23 |
24 | val diceService = new DiceService {
25 | override def throwDice: Future[Int] = Future.successful(4)
26 | }
27 | val randomNumberService =
28 | new DiceDrivenRandomNumberService(diceService)
29 |
30 | whenReady(randomNumberService.generateRandomNumber) { result =>
31 | result shouldBe(4)
32 | }
33 |
34 | }
35 |
36 | it should "be able to cope with problematic dice throws" in {
37 | val overzealousDiceThrowingService = new DiceService {
38 | val counter = new AtomicInteger()
39 | override def throwDice: Future[Int] = {
40 | val count = counter.incrementAndGet()
41 | if(count % 2 == 0) {
42 | Future.successful(4)
43 | } else {
44 | Future.failed(new RuntimeException(
45 | "Dice fell of the table and the cat won't give it back"
46 | ))
47 | }
48 | }
49 | }
50 |
51 | val randomNumberService =
52 | new DiceDrivenRandomNumberService(overzealousDiceThrowingService)
53 |
54 | whenReady(randomNumberService.generateRandomNumber) { result =>
55 | result shouldBe(4)
56 | }
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | These are the source code that goes along with the book "Reactive Web Applications" (http://manning.com/bernhardt/)
2 |
--------------------------------------------------------------------------------
/listings/CH10.md:
--------------------------------------------------------------------------------
1 | ### Listing 10.1
2 |
3 | ```
4 | $(document).ready(function () {
5 | $('#button').on('click', function () {
6 | $('#text').text('Hello');
7 | });
8 | });
9 | ```
10 |
11 | ### Listing 10.2
12 |
13 | ```
14 | @(message: String)
15 | @main(message) {
16 |
17 |
18 | }
19 | ```
20 |
21 | ### Listing 10.3
22 |
23 | ```
24 | import org.scalatest._
25 | import play.api.test._
26 | import play.api.test.Helpers._
27 | import org.scalatestplus.play._
28 |
29 | class ApplicationSpec
30 | extends PlaySpec
31 | with OneServerPerSuite
32 | with OneBrowserPerSuite
33 | with FirefoxFactory {
34 | "The Application" must {
35 | "display a text when clicking on a button" in {
36 | go to (s"http://localhost:$port")
37 | pageTitle mustBe "Hello"
38 | click on find(id("button")).value
39 | eventually {
40 | val expectedText = app.configuration.getString("text")
41 | find(id("text")).map(_.text) mustBe expectedText
42 | }
43 | }
44 | }
45 | }
46 |
47 | ```
48 |
49 | ### Listing 10.4
50 |
51 | ```
52 | name := """ch10"""
53 |
54 | version := "1.0-SNAPSHOT"
55 |
56 | lazy val root = (project in file(".")).enablePlugins(
57 | PlayScala
58 | )
59 |
60 | scalaVersion := "2.11.7"
61 |
62 | libraryDependencies ++= Seq(
63 | "org.webjars" %% "webjars-play" % "2.4.0-1",
64 | "org.webjars" % "jquery" % "2.1.4",
65 | "org.scalatest" %% "scalatest" % "2.2.1" % "test",
66 | "org.scalatestplus" %% "play" % "1.4.0-M4" % "test"
67 | )
68 |
69 | routesGenerator := InjectedRoutesGenerator
70 |
71 | pipelineStages := Seq(rjs)
72 |
73 | RjsKeys.mainModule := "application"
74 |
75 | RjsKeys.mainConfig := "application"
76 |
77 | ```
78 |
79 | ### Listing 10.5
80 |
81 | ```
82 | (function (requirejs) {
83 | 'use strict';
84 | requirejs.config({
85 | shim: {
86 | 'jsRoutes': {
87 | deps: [],
88 | exports: 'jsRoutes'
89 | }
90 | },
91 | paths: {
92 | 'jquery': ['../lib/jquery/jquery']
93 | }
94 | });
95 | requirejs.onError = function (err) {
96 | console.log(err);
97 | };
98 | require(['jquery'], function ($) {
99 | $(document).ready(function () {
100 | // ...
101 | });
102 | });
103 | })(requirejs);
104 | ```
105 |
106 | ### Listing 10.6
107 |
108 | ```
109 | docker run
110 | --name chapter10-jenkins
111 | -p 8080:8080
112 | -v ~/reactive-web-applications/CH10:/var/jenkins_home/ch10
113 | docker-jenkins
114 | ```
115 |
116 | ### Listing 10.7
117 |
118 | ```
119 | maintainer := "John Doe "
120 |
121 | packageSummary in Linux := "Chapter 10 of Reactive Web Applications"
122 |
123 | packageDescription :=
124 | "This package installs the Play Application used as an example
125 | in Chapter 10 of the book Reactive Web Applications"
126 |
127 | serverLoading in Debian := ServerLoader.Systemd
128 |
129 | ```
130 |
--------------------------------------------------------------------------------