├── 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 | 9 | 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 | --------------------------------------------------------------------------------