├── .gitignore ├── README.md ├── chapter-10 └── ansible-pageviewcounter │ ├── app-task.yaml │ ├── db-task.yaml │ ├── docker-task.yaml │ ├── hosts │ ├── lb-task.yaml │ └── play.yaml ├── chapter-11 ├── 0-dockerize │ └── Readme.md ├── 1-deploy-all │ ├── 00-sercets-cmaps.yml │ ├── README.md │ ├── auth-app │ │ ├── 10-cmaps.yml │ │ ├── 10-secrets.yml │ │ ├── deployment.yml │ │ └── service.yml │ ├── so-app │ │ └── so-app.yml │ └── web-app │ │ └── web-app.yml ├── 2-scaling-cluster │ ├── README.md │ └── auth-app.yml └── 3-rolling-updates │ ├── README.md │ └── web-app.yml ├── chapter-4 ├── .gitignore ├── README.md ├── auth-app │ ├── Dockerfile │ ├── app │ │ ├── auth │ │ │ ├── UserController.scala │ │ │ ├── UserDao.scala │ │ │ └── UserService.scala │ │ ├── tokens │ │ │ ├── TokenController.scala │ │ │ ├── TokenDao.scala │ │ │ └── TokenService.scala │ │ └── utils │ │ │ └── Contexts.scala │ └── conf │ │ ├── application.conf │ │ ├── evolutions │ │ └── default │ │ │ └── 1.sql │ │ └── routes ├── build.sbt ├── commons │ └── src │ │ └── main │ │ └── scala │ │ └── com │ │ └── microservices │ │ ├── auth │ │ └── User.scala │ │ └── search │ │ └── Search.scala ├── project │ ├── build.properties │ └── plugins.sbt ├── rank-app │ ├── app │ │ ├── controller │ │ │ └── RankController.scala │ │ └── utils │ │ │ └── RankProperties.scala │ └── conf │ │ ├── application.conf │ │ └── routes ├── so-app │ ├── Dockerfile │ ├── app │ │ ├── controllers │ │ │ └── SOSearchController.scala │ │ ├── dao │ │ │ └── SearchDao.scala │ │ ├── service │ │ │ └── SearchService.scala │ │ └── users │ │ │ └── Contexts.scala │ ├── conf │ │ ├── application.conf │ │ ├── evolutions │ │ │ └── default │ │ │ │ └── 1.sql │ │ └── routes │ └── test │ │ └── dataGen │ │ └── DataGen.scala └── web-app │ ├── Dockerfile │ ├── app │ ├── assets │ │ ├── javascripts │ │ │ ├── app-start.js │ │ │ ├── components │ │ │ │ ├── AppRoutes.js │ │ │ │ ├── DashboardPage.js │ │ │ │ ├── Flag.js │ │ │ │ ├── IndexPage.js │ │ │ │ ├── Layout.js │ │ │ │ ├── Medal.js │ │ │ │ ├── NotFoundPage.js │ │ │ │ ├── RegisterPage.js │ │ │ │ ├── SearchBar.js │ │ │ │ └── SearchResults.js │ │ │ ├── package.json │ │ │ ├── routes.js │ │ │ └── webpack.config.js │ │ └── stylesheets │ │ │ └── style.css │ ├── controller │ │ ├── LoginController.scala │ │ ├── SearchController.scala │ │ └── SecurityAction.scala │ ├── parser │ │ └── QueryParser.scala │ └── utils │ │ └── AllProperties.scala │ ├── conf │ ├── application.conf │ └── routes │ ├── public │ ├── css │ │ ├── semantic.min.css │ │ └── style.css │ ├── favicon.ico │ ├── img │ │ ├── driulis-gonzalez-cover.jpg │ │ ├── driulis-gonzalez.jpg │ │ ├── flag-cu.png │ │ ├── flag-fr.png │ │ ├── flag-jp.png │ │ ├── flag-nl.png │ │ ├── flag-uz.png │ │ ├── logo-judo-heroes.png │ │ ├── mark-huizinga-cover.jpg │ │ ├── mark-huizinga.jpg │ │ ├── medal.png │ │ ├── rishod-sobirov-cover.jpg │ │ ├── rishod-sobirov.jpg │ │ ├── ryoko-tani-cover.jpg │ │ ├── ryoko-tani.jpg │ │ ├── teddy-riner-cover.jpg │ │ └── teddy-riner.jpg │ ├── index-static.html │ └── js │ │ └── bundle.js │ └── test │ └── parser │ └── QueryParserTest.scala ├── chapter-9 └── docker-flask │ ├── docker-compose.yml │ ├── hello-service │ ├── Dockerfile │ └── server.py │ └── time-service │ ├── Dockerfile │ └── time_server.py ├── chirper-app-complete ├── .gitignore ├── LICENSE ├── README.md ├── activity-stream-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── activity │ │ └── api │ │ ├── ActivityStreamService.scala │ │ └── HistoricalActivityStreamReq.scala ├── activity-stream-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── activity │ │ │ └── impl │ │ │ ├── ActivityStreamModule.scala │ │ │ └── ActivityStreamServiceImpl.scala │ │ └── test │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── activity │ │ └── impl │ │ └── ActivityStreamServiceTest.scala ├── build.sbt ├── chirp-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── chirp │ │ └── api │ │ ├── Chirp.scala │ │ ├── ChirpService.scala │ │ ├── HistoricalChirpsRequest.scala │ │ └── LiveChirpsRequest.scala ├── chirp-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── chirp │ │ │ └── impl │ │ │ ├── ChipServiceImpl.scala │ │ │ └── ChirpModule.scala │ │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── chirp │ │ └── impl │ │ └── ChirpServiceTest.scala ├── friend-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── friend │ │ └── api │ │ ├── FriendId.scala │ │ ├── FriendService.scala │ │ └── User.scala ├── friend-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── friend │ │ │ └── impl │ │ │ ├── FriendCommand.scala │ │ │ ├── FriendEntity.scala │ │ │ ├── FriendEventProcessor.scala │ │ │ ├── FriendEvents.scala │ │ │ ├── FriendModule.scala │ │ │ ├── FriendServiceImpl.scala │ │ │ └── FriendState.scala │ │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── friend │ │ └── impl │ │ ├── FriendEntityTest.scala │ │ └── FriendServiceTest.scala ├── friend-recommendation-api │ └── src │ │ └── main │ │ └── scala │ │ └── com │ │ └── scalamicroservices │ │ └── rec │ │ └── impl │ │ └── FriendRecService.scala ├── friend-recommendation-impl │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── com │ │ └── scalamicroservices │ │ └── rec │ │ └── impl │ │ ├── FriendRecModule.scala │ │ └── FriendRecServiceImpl.scala ├── front-end │ ├── app │ │ ├── FrontEndLoader.scala │ │ ├── assets │ │ │ ├── circuitbreaker.jsx │ │ │ ├── main.css │ │ │ └── main.jsx │ │ ├── controllers │ │ │ └── MainController.scala │ │ └── views │ │ │ ├── circuitbreaker.scala.html │ │ │ └── index.scala.html │ ├── bundle-configuration │ │ └── default │ │ │ └── runtime-config.sh │ └── conf │ │ ├── application.conf │ │ └── routes ├── project │ ├── build.properties │ └── plugins.sbt └── tutorial │ └── index.html ├── chirper-app-primer ├── .gitignore ├── .toDelete ├── LICENSE ├── README.md ├── activity-stream-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── activity │ │ └── api │ │ ├── ActivityStreamService.scala │ │ └── HistoricalActivityStreamReq.scala ├── activity-stream-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── activity │ │ │ └── impl │ │ │ ├── ActivityStreamModule.scala │ │ │ └── ActivityStreamServiceImpl.scala │ │ └── test │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── activity │ │ └── impl │ │ └── ActivityStreamServiceTest.scala ├── build.sbt ├── chirp-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── chirp │ │ └── api │ │ ├── Chirp.scala │ │ ├── ChirpService.scala │ │ ├── HistoricalChirpsRequest.scala │ │ └── LiveChirpsRequest.scala ├── chirp-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── chirp │ │ │ └── impl │ │ │ ├── ChipServiceImpl.scala │ │ │ └── ChirpModule.scala │ │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── chirp │ │ └── impl │ │ └── ChirpServiceTest.scala ├── friend-api │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── friend │ │ └── api │ │ ├── FriendId.scala │ │ ├── FriendService.scala │ │ └── User.scala ├── friend-impl │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── application.conf │ │ └── scala │ │ │ └── sample │ │ │ └── chirper │ │ │ └── friend │ │ │ └── impl │ │ │ ├── FriendModule.scala │ │ │ └── FriendServiceImpl.scala │ │ └── test │ │ ├── resources │ │ └── logback-test.xml │ │ └── scala │ │ └── sample │ │ └── chirper │ │ └── friend │ │ └── impl │ │ ├── FriendEntityTest.scala │ │ └── FriendServiceTest.scala ├── front-end │ ├── app │ │ ├── FrontEndLoader.scala │ │ ├── assets │ │ │ ├── circuitbreaker.jsx │ │ │ ├── main.css │ │ │ └── main.jsx │ │ ├── controllers │ │ │ └── MainController.scala │ │ └── views │ │ │ ├── circuitbreaker.scala.html │ │ │ └── index.scala.html │ ├── bundle-configuration │ │ └── default │ │ │ └── runtime-config.sh │ └── conf │ │ ├── application.conf │ │ └── routes ├── project │ ├── build.properties │ └── plugins.sbt └── tutorial │ └── index.html ├── first-app ├── app │ ├── controllers │ │ ├── HomeController.scala │ │ ├── LoginController.scala │ │ └── Users.scala │ ├── json │ │ ├── JsonReadExamples.scala │ │ └── JsonWriteExamples.scala │ └── views │ │ └── index.scala.html ├── build.sbt ├── conf │ ├── application.conf │ └── routes └── project │ ├── build.properties │ └── plugins.sbt └── search-app ├── .gitignore ├── app ├── controllers │ ├── Application.scala │ └── SearchController.scala ├── service │ ├── SearchService.scala │ └── impl │ │ └── SearchServiceImpl.scala ├── utils │ ├── AppExecutionContext.scala │ └── Contexts.scala └── views │ └── index.scala.html ├── build.sbt ├── conf ├── application.conf └── routes ├── project ├── build.properties └── plugins.sbt ├── public ├── images │ └── favicon.png ├── javascripts │ └── jquery-1.9.0.min.js └── stylesheets │ └── main.css └── test ├── IntegrationSpec.scala └── service └── impl └── SearchSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.class 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .idea 7 | out 8 | node_modules 9 | babel_cache 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # book-examples 2 | 3 | This repository has all the code examples for the scala microservices book divided by chapters. Each example is self containers, until specified otherwise. 4 | 5 | ## Tools 6 | - All examples are written in Scala, except few which are written in Python. 7 | - Basic understanding of Docker. 8 | 9 | 10 | ## Feedback and Bugs 11 | 12 | please email purijatin@**ail.com 13 | 14 | -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/app-task.yaml: -------------------------------------------------------------------------------- 1 | 2 | - name: pull latest docker image 3 | command: docker pull chartotu/pageviewcounter:latest 4 | - name: start docker container with redis-server url as environment variable. 5 | command: docker run -d -e "REDIS_HOST={{ hostvars.databases }}" -p "{{ http_port }}:{{ http_port }}" chartotu/pageviewcounter 6 | -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/db-task.yaml: -------------------------------------------------------------------------------- 1 | - name: update apt 2 | apt: update_cache=yes 3 | - name: install redis-server 4 | apt: name=redis-server state=present 5 | - name: start server 6 | systemd: name=redis-server state=started -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/docker-task.yaml: -------------------------------------------------------------------------------- 1 | - apt_key: url="https://download.docker.com/linux/ubuntu/gpg" 2 | state=present 3 | - name: add deb repo 4 | command: add-apt-repository "deb [arch=amd64] 5 | https://download.docker.com/linux/ubuntu $(lsb_release -cs) 6 | stable" 7 | - name: update apt 8 | apt: update_cache=yes 9 | - name: install docker 10 | apt: name=docker-ce state=present 11 | -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/hosts: -------------------------------------------------------------------------------- 1 | [dockerhosts] 2 | localhost 3 | [webservers] 4 | localhost 5 | [databases] 6 | localhost 7 | [loadbalancer] 8 | localhost 9 | -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/lb-task.yaml: -------------------------------------------------------------------------------- 1 | - name: update apt 2 | apt: update_cache=yes 3 | - name: install nginx 4 | apt: name=nginx state=present 5 | - name: create nginx configuration template 6 | template: 7 | src: nginx.conf.j2 8 | dest: /etc/nginx/sites-available/pageviewcounter 9 | owner: nginx 10 | group: nginx 11 | mode: 0644 12 | - name: restart nginx 13 | systemd: name=nginx state=restart -------------------------------------------------------------------------------- /chapter-10/ansible-pageviewcounter/play.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: databases 3 | vars: 4 | http_port: 6379 5 | remote_user: system 6 | tasks: 7 | - include: db-task.yaml 8 | 9 | - hosts: dockerhosts 10 | vars: 11 | remote_user: system 12 | tasks: 13 | - include: docker-task.yaml 14 | 15 | - hosts: webservers 16 | vars: 17 | http_port: 80 18 | remote_user: system 19 | tasks: 20 | - include: app-task.yaml 21 | 22 | - hosts: loadbalancers 23 | vars: 24 | remote_user: system 25 | tasks: 26 | - include: lb-task.yaml 27 | -------------------------------------------------------------------------------- /chapter-11/0-dockerize/Readme.md: -------------------------------------------------------------------------------- 1 | ## Docker install 2 | 3 | Follow the [installation guide](https://docs.docker.com/engine/installation/). 4 | 5 | ## Docker build 6 | 7 | ```shell 8 | $ cd ../chapter-4/ 9 | $ docker build -t auth-app . 10 | ``` 11 | 12 | ## Docker run 13 | 14 | ```shell 15 | $ cd ../chapter-4/ 16 | $ docker run -d -e APP_SECRET="adqwlne2p123" -e HTTP_SECRET="324kjb23233WQ" -p 9000:9000 auth-app 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/00-sercets-cmaps.yml: -------------------------------------------------------------------------------- 1 | Kind -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/README.md: -------------------------------------------------------------------------------- 1 | ## 1-deploy-all 2 | 3 | In this tutorial, we will deploy pods and services for all 3 microservices of the seeker example app. 4 | 5 | We will create a new namespace called `sm-seeker` 6 | 7 | ```bash 8 | @ kubectl create namespace "sm-seeker" 9 | ``` 10 | 11 | Deploy all pods and services. 12 | ```bash 13 | $ cd auth-app/ 14 | $ kubectl apply -f 10-cmaps.yml -n sm-seeker 15 | $ kubectl apply -f 10-secrets.yml -n sm-seeker 16 | $ kubectl apply -f deployment.yml -n sm-seeker 17 | $ kubectl apply -f service.yml -n sm-seeker 18 | 19 | Repeat this for so-app and web-app. 20 | ``` 21 | 22 | Check our deployments. 23 | ```bash 24 | $ kubectl get deployments -n seeker 25 | $ kubectl get services -n seeker 26 | $ kubectl get pods -n seeker 27 | ``` 28 | 29 | Now visit http://127.0.0.1:8001/api/v1/proxy/namespaces/sm-seeker/services/auth-service:2000/ 30 | 31 | You should see play framework error page. -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/auth-app/10-cmaps.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: sm-cmaps-auth 5 | data: 6 | application.conf: | 7 | slick.dbs.default.driver="slick.driver.H2Driver$" 8 | slick.dbs.default.db.driver="org.h2.Driver" 9 | slick.dbs.default.db.url="jdbc:h2:~/h2" 10 | slick.dbs.default.db.user=sa 11 | slick.dbs.default.db.password="" 12 | 13 | #run conf/userdb/1.sql by userdb 14 | //play.applyEvolutions.default=true 15 | play.evolutions.autoApply=true 16 | 17 | evolutionplugin=enabled 18 | play.evolutions.db.default.autoApply=true 19 | play.evolutions.db.default.autoApplyDowns=true 20 | 21 | contexts { 22 | db-lookups{ 23 | throughput = 1 24 | thread-pool-executor { 25 | fixed-pool-size = 10 26 | } 27 | } 28 | 29 | cpu-operations { 30 | fork-join-executor { 31 | parallelism-max = 2 32 | } 33 | } 34 | } 35 | 36 | 37 | token { 38 | ttl = 86400000 # seconds 39 | } -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/auth-app/10-secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: sm-secrets-auth 5 | type: Opaque 6 | data: 7 | # actual value: asdjb2312312edqwd 8 | HTTP_SECRET: YXNkamIyMzEyMzEyZWRxd2Q= 9 | #actual value: las202buqb3212edqw 10 | APP_SECRET: bGFzMjAyYnVxYjMyMTJlZHF3 -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/auth-app/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: auth-deployment 5 | labels: 6 | app: auth 7 | environment: production 8 | spec: 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: auth 14 | environment: production 15 | spec: 16 | volumes: 17 | - name: sm-cmaps-auth-volume 18 | configMap: 19 | name: sm-cmaps-auth 20 | containers: 21 | - name: auth-container 22 | imagePullPolicy: Always 23 | image: chartotu/auth-app:latest 24 | workingDir: 25 | # command: [ "/bin/sh","-c","while true;do java -version; sleep 2; done" ] 26 | env: 27 | - name: APP_SECRET 28 | valueFrom: 29 | secretKeyRef: 30 | name: sm-secrets-auth 31 | key: APP_SECRET 32 | - name: HTTP_SECRET 33 | valueFrom: 34 | secretKeyRef: 35 | name: sm-secrets-auth 36 | key: HTTP_SECRET 37 | volumeMounts: 38 | - name: sm-cmaps-auth-volume 39 | mountPath: /opt/auth-app/conf/application.conf 40 | readOnly: true -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/auth-app/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-service 5 | labels: 6 | app: auth 7 | environment: production 8 | annotations: 9 | description: Authenication for our seeker application 10 | spec: 11 | type: LoadBalancer 12 | sessionAffinity: None 13 | ports: 14 | - name: http-2000 15 | port: 2000 16 | targetPort: 9000 17 | protocol: TCP 18 | selector: 19 | app: auth 20 | environment: production -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/so-app/so-app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: so-service 5 | labels: 6 | app: so 7 | environment: production 8 | annotations: 9 | description: Stackoverflow search service 10 | spec: 11 | type: LoadBalancer 12 | sessionAffinity: None 13 | ports: 14 | - name: http-4000 15 | port: 4000 16 | targetPort: 9000 17 | protocol: TCP 18 | selector: 19 | app: so 20 | environment: production 21 | 22 | --- 23 | 24 | apiVersion: extensions/v1beta1 25 | kind: Deployment 26 | metadata: 27 | name: so-deployment 28 | labels: 29 | app: so 30 | environment: production 31 | spec: 32 | replicas: 2 33 | template: 34 | metadata: 35 | labels: 36 | app: so 37 | environment: production 38 | spec: 39 | containers: 40 | - name: so-container 41 | imagePullPolicy: Always 42 | image: chartotu/so-app:latest 43 | # command: [ "/bin/sh","-c","while true;do java -version; sleep 2; done" ] 44 | env: 45 | - name: APP_SECRET 46 | value: "8gasd2312rgwe,ewewew" 47 | - name: HTTP_SECRET 48 | value: "342w9erwj32" 49 | -------------------------------------------------------------------------------- /chapter-11/1-deploy-all/web-app/web-app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-service 5 | labels: 6 | app: auth 7 | environment: production 8 | annotations: 9 | description: Authenication for our seeker application 10 | spec: 11 | type: LoadBalancer 12 | sessionAffinity: None 13 | ports: 14 | - name: http-3000 15 | port: 3000 16 | targetPort: 9000 17 | protocol: TCP 18 | selector: 19 | app: auth 20 | environment: production 21 | 22 | --- 23 | 24 | apiVersion: extensions/v1beta1 25 | kind: Deployment 26 | metadata: 27 | name: auth-deployment 28 | labels: 29 | app: auth 30 | environment: production 31 | spec: 32 | replicas: 2 33 | template: 34 | metadata: 35 | labels: 36 | app: auth 37 | environment: production 38 | spec: 39 | containers: 40 | - name: auth-container 41 | imagePullPolicy: Always 42 | image: chartotu/web-app:latest 43 | # command: [ "/bin/sh","-c","while true;do java -version; sleep 2; done" ] 44 | env: 45 | - name: APP_SECRET 46 | value: "8gasd2312rgwe,ewewew" 47 | - name: HTTP_SECRET 48 | value: "342w9erwj32" 49 | 50 | -------------------------------------------------------------------------------- /chapter-11/2-scaling-cluster/README.md: -------------------------------------------------------------------------------- 1 | ## 2-scaling-cluster 2 | 3 | In this tutorial, we are scaling app the authentication application. By increasing the replicas to 4 from 2, we request kubernetes api controller to schedule 2 more copies of auth-pod in the cluster. 4 | 5 | The service is unimpacted during the whole process. On successful pod start, they are picked up by the `auth-service` . Now this service will load balance traffic to 4 pods. 6 | 7 | 8 | ```bash 9 | $ kubectl apply -f auth-app.yml -n seeker 10 | ``` -------------------------------------------------------------------------------- /chapter-11/2-scaling-cluster/auth-app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: auth-deployment 5 | labels: 6 | app: auth 7 | environment: production 8 | spec: 9 | replicas: 4 10 | template: 11 | metadata: 12 | labels: 13 | app: auth 14 | environment: production 15 | spec: 16 | containers: 17 | - name: auth-container 18 | imagePullPolicy: Always 19 | image: chartotu/auth-app:latest 20 | # command: [ "/bin/sh","-c","while true;do java -version; sleep 2; done" ] 21 | env: 22 | - name: APP_SECRET 23 | value: "8gasd2312rgwe,ewewew" 24 | - name: HTTP_SECRET 25 | value: "342w9erwj32" 26 | 27 | -------------------------------------------------------------------------------- /chapter-11/3-rolling-updates/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-11/3-rolling-updates/README.md -------------------------------------------------------------------------------- /chapter-11/3-rolling-updates/web-app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: auth-deployment 5 | labels: 6 | app: auth 7 | environment: production 8 | spec: 9 | replicas: 2 10 | template: 11 | metadata: 12 | labels: 13 | app: auth 14 | environment: production 15 | spec: 16 | containers: 17 | - name: auth-container 18 | imagePullPolicy: Always 19 | image: chartotu/web-app:v2 20 | # command: [ "/bin/sh","-c","while true;do java -version; sleep 2; done" ] 21 | env: 22 | - name: APP_SECRET 23 | value: "8gasd2312rgwe,ewewew" 24 | - name: HTTP_SECRET 25 | value: "342w9erwj32" 26 | 27 | -------------------------------------------------------------------------------- /chapter-4/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | .idea/ 4 | -------------------------------------------------------------------------------- /chapter-4/README.md: -------------------------------------------------------------------------------- 1 | ## Talent Search Engine 2 | A talent search application to search developers across a city for a technology. 3 | 4 | The aim of this application is to achieve two things: 5 | * Learn more about Play by using its API: ActionBuilders, WSClient, configurations etc 6 | * Understand the difficulties faces in such architectures. So that we are better prepared to address these problems going forward in 7 | other chapters. 8 | 9 | ### Run 10 | To run the code: `sbt runAll` 11 | 12 | And then connect to `http://localhost:3000`. After logging in, you may search by entering: 13 | Scala developers in London 14 | 15 | 16 | ### Notes 17 | * If the program results in `OutofMemoryError` when running with `runAll` command, provide the memory flag: `sbt -mem 2000 runAll`. In this case, it means 2000mb of ram. 18 | * On Intellij, in case you are unable to load the project, then refer: http://stackoverflow.com/questions/41966066/jboss-interceptor-api-1-1-not-found-when-added-as-sbt-dependency 19 | -------------------------------------------------------------------------------- /chapter-4/auth-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM anapsix/alpine-java 2 | 3 | MAINTAINER Selvam Palanimalai 4 | 5 | 6 | COPY target/universal/auth-app-0.1-SNAPSHOT.zip /opt/scala/auth-app.zip 7 | 8 | RUN unzip /opt/scala/auth-app.zip -d /opt/scala/ 9 | 10 | CMD /opt/scala/auth-app-0.1-SNAPSHOT/bin/auth-app -Dplay.http.secret.key=$HTTP_SECRET -Dplay.crypto.secret=$APP_SECRET 11 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/auth/UserController.scala: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth._ 6 | import play.api.Logger 7 | import play.api.libs.json.Json 8 | import play.api.mvc._ 9 | import tokens.TokenService 10 | import utils.Contexts 11 | 12 | import scala.concurrent.Future 13 | 14 | /** 15 | * The usernames along with hash of the passwords are stored in h2 database 16 | */ 17 | @Singleton 18 | class UserController @Inject()(userService: UserService, contexts: Contexts, tokenService: TokenService, cc: ControllerComponents) extends AbstractController(cc) { 19 | 20 | implicit val executionContext = contexts.cpuLookup 21 | 22 | /** 23 | * Function to register a new user created. 24 | * 25 | * Expects a json corresponding to the `com.microservices.auth.User` object in the request body. It creates a token and responds 26 | * the token to the caller. 27 | */ 28 | /* 29 | Example call: 30 | curl -X POST \ 31 | http://localhost:5001/v1/auth/register \ 32 | -H 'content-type: application/json' \ 33 | -d '{ 34 | "email":"p@p.com", 35 | "password":"abcd" 36 | }' 37 | */ 38 | def register = Action.async(parse.json) { implicit request => 39 | request.body.validate[User].fold( 40 | error => Future.successful(BadRequest("Not a valid input format: " + error.mkString)), 41 | user => 42 | userService.userExists(user.email).flatMap(ifExists => { 43 | if (ifExists) 44 | Future.successful(BadRequest(s"User already exists: ${user.email}. cannot register again")) 45 | else { 46 | userService.addUser(user) 47 | .flatMap(_ => tokenService.createToken(user.email)) 48 | .map(x => Ok(Json.toJson(x.token))) 49 | } 50 | }) 51 | ) 52 | } 53 | 54 | /** 55 | * Verifies a user based on username and password. If valid user then returns the token belonging to that user 56 | */ 57 | /* 58 | curl -X POST \ 59 | http://localhost:5001/v1/auth/login \ 60 | -H 'content-type: application/json' \ 61 | -d '{ 62 | "email":"p@p.com", 63 | "password":"abcd" 64 | }' 65 | */ 66 | def login = Action.async(parse.json) { implicit request => 67 | request.body.validate[User].fold( 68 | error => Future.successful(BadRequest("Not a valid input format: " + error.mkString)), 69 | user => 70 | userService.validateUser(user.email, user.password).flatMap { validated => 71 | if (validated) 72 | tokenService.createToken(user.email).map(x => Ok(Json.toJson(x.token))) 73 | else Future.successful(BadRequest("username/password mismatch")) 74 | } 75 | ) 76 | } 77 | 78 | /** 79 | * logsout the user be deleting the token associated with the user 80 | */ 81 | def logout(token: String) = Action.async { implicit request => 82 | val future = tokenService.authenticateToken(TokenStr(token), refresh = false) 83 | future.map(x => { 84 | tokenService.dropToken(x.token) 85 | Ok("loggedout") 86 | }).recoverWith { 87 | case e: Exception => Future.successful(BadRequest(e.getMessage)) 88 | } 89 | } 90 | 91 | 92 | def getAll = Action.async { 93 | userService.getAllUserNames.map(x => Ok(Json.toJson(x))) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/auth/UserDao.scala: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import javax.inject.Singleton 4 | 5 | import com.google.inject.Inject 6 | import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} 7 | import slick.jdbc.JdbcProfile 8 | import slick.jdbc.GetResult 9 | 10 | import scala.concurrent.Future 11 | 12 | case class UserAuth(email:String, passwdHash:String, creationTime:Long) 13 | 14 | @Singleton 15 | class UserDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] { 16 | 17 | 18 | import profile.api._ 19 | 20 | implicit val getUserResult = GetResult(r => UserAuth(r.nextString, r.nextString, r.nextLong())) 21 | 22 | 23 | def getUserByEmail(email:String): Future[Option[UserAuth]] = { 24 | db.run(sql"select email, passwdHash, creationTime from users where email = $email".as[UserAuth].headOption) 25 | } 26 | 27 | def createUser(user:UserAuth): Future[Int] = { 28 | db.run(sqlu"insert into users (email, passwdHash, creationTime) values (${user.email}, ${user.passwdHash}, ${user.creationTime})") 29 | } 30 | 31 | def getAllUsers: Future[Vector[String]] = { 32 | 33 | db.run(sql"select email from users".as[String]) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/auth/UserService.scala: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth.User 6 | import org.mindrot.jbcrypt.BCrypt 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | 10 | @Singleton 11 | class UserService @Inject()(userDao: UserDao) { 12 | def userExists(email: String)(implicit exec:ExecutionContext): Future[Boolean] = { 13 | for { 14 | user <- userDao.getUserByEmail(email) 15 | } yield { 16 | user.isDefined 17 | } 18 | } 19 | 20 | def addUser(user: User)(implicit exec: ExecutionContext): Future[Int] = { 21 | userExists(user.email).flatMap(userExists => if (userExists) 22 | throw new IllegalArgumentException("User already exists") 23 | else userDao.createUser(UserAuth(user.email, hashPassword(user.password), System.currentTimeMillis()))) 24 | } 25 | 26 | def validateUser(email:String, password:String)(implicit exec:ExecutionContext): Future[Boolean] = { 27 | userDao.getUserByEmail(email).map { 28 | case Some(auth) => checkPassword(password, auth.passwdHash) 29 | case None => false 30 | } 31 | } 32 | 33 | def getAllUserNames: Future[Vector[String]] = userDao.getAllUsers 34 | 35 | private def hashPassword(password: String): String = BCrypt.hashpw(password, BCrypt.gensalt(11)) 36 | 37 | private def checkPassword(password: String, passwordHash: String): Boolean = BCrypt.checkpw(password, passwordHash) 38 | } 39 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/tokens/TokenController.scala: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth.{ResponseObj, Token, TokenStr} 6 | import play.api.libs.json.Json 7 | import play.api.mvc.{AbstractController, ControllerComponents} 8 | import utils.Contexts 9 | 10 | import scala.concurrent.Future 11 | 12 | @Singleton 13 | class TokenController @Inject()(contexts: Contexts, tokenService: TokenService, cc: ControllerComponents) extends AbstractController(cc) { 14 | implicit val executionContext = contexts.cpuLookup 15 | 16 | /** 17 | * Refreshes the token for a user with a new token. 18 | * 19 | * @param token the token of the user 20 | * @return 21 | */ 22 | /* 23 | curl -X GET http://localhost:5001/v1/tokens/refresh/677678f7-5dc9-4236-a254-c067b0662e8c 24 | */ 25 | def refreshToken(token: String) = Action.async { 26 | tokenService.authenticateToken(TokenStr(token), true).map(x => Ok(Json.toJson(x))) 27 | } 28 | 29 | /** 30 | * Authenticates a user based on the token provided. Returns the token object if success. 31 | * Else returns a BadRequest 32 | * 33 | * @param token 34 | * @return 35 | */ 36 | /* 37 | Sample call (the token would need to be replaced with your generated token) 38 | curl -X GET http://localhost:5001/v1/tokens/authenticate/677678f7-5dc9-4236-a254-c067b0662e8c 39 | */ 40 | def authenticate(token: String) = Action.async { 41 | tokenService.authenticateToken(TokenStr(token), true) 42 | .map((x: Token) => Ok(Json.toJson(x))) 43 | .recoverWith { 44 | case e: Exception => Future.successful(BadRequest(Json.toJson(e.getMessage))) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/tokens/TokenDao.scala: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth.{Token, TokenStr} 6 | import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} 7 | import slick.jdbc.JdbcProfile 8 | import slick.jdbc.GetResult 9 | import utils.Contexts 10 | 11 | import scala.concurrent.Future 12 | 13 | @Singleton 14 | class TokenDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, contexts: Contexts) extends HasDatabaseConfigProvider[JdbcProfile] { 15 | implicit val executionContext = contexts.cpuLookup 16 | 17 | import profile.api._ 18 | 19 | implicit val getTokenResult = GetResult(r => Token(TokenStr(r.nextString), r.nextLong(), r.nextString)) 20 | 21 | 22 | def getToken(token:String): Future[Option[Token]] = { 23 | db.run(sql"select token, validTill,key from tokens where token = $token".as[Token].headOption) 24 | } 25 | 26 | def getTokenFromkey(key:String): Future[Option[Token]] = { 27 | db.run(sql"select token, validTill,key from tokens where key = $key".as[Token].headOption) 28 | } 29 | 30 | def createToken(token:Token): Future[Int] = { 31 | db.run(sqlu"insert into tokens (key, token, validTill) values (${token.key}, ${token.token.tokenStr}, ${token.validTill})") 32 | } 33 | 34 | def deleteToken(token: String)={ 35 | db.run(sqlu"delete from tokens where token=${token}") 36 | } 37 | 38 | def updateTTL(token: String, till:Long)={ 39 | db.run(sqlu"update tokens set validTill=$till where token=${token}") 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/tokens/TokenService.scala: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import java.util.UUID 4 | import javax.inject.{Inject, Singleton} 5 | 6 | import com.microservices.auth.{Token, TokenStr} 7 | import utils.Contexts 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | 11 | 12 | @Singleton 13 | class TokenService @Inject()(context: Contexts, tokensDao: TokenDao) { 14 | /** 15 | * Creates a token based on the key provided. If there was already a token generated for the key and is valid, then the same token is returned 16 | * Else a new token is generated and returned 17 | * @param key key for example user email 18 | * @return 19 | */ 20 | def createToken(key: String)(implicit exec:ExecutionContext): Future[Token] = { 21 | tokensDao.getTokenFromkey(key).flatMap { 22 | case Some(token) => 23 | if(token.validTill <= System.currentTimeMillis()){ 24 | dropToken(token.token) 25 | val newToken: Token = generateToken(key) 26 | tokensDao.createToken(newToken).map(_ => newToken) 27 | } else{ 28 | Future(token) 29 | } 30 | case None => 31 | val newToken = generateToken(key) 32 | tokensDao.createToken(newToken).map(_ => newToken) 33 | } 34 | } 35 | 36 | /** 37 | * verifies if its a valid token. Returns a future completed with token if so. Else the returned future completes with an exception 38 | */ 39 | def authenticateToken(token: TokenStr, refresh:Boolean)(implicit exec:ExecutionContext): Future[Token] = { 40 | tokensDao.getToken(token.tokenStr).map{ 41 | case Some(t) => 42 | if (t.validTill < System.currentTimeMillis()) 43 | throw new IllegalArgumentException("Token expired.") 44 | else { 45 | if(refresh) { 46 | val max = maxTTL 47 | tokensDao.updateTTL(token.tokenStr, max) 48 | Token(t.token, max, t.key) 49 | }else t 50 | } 51 | case None => throw new IllegalArgumentException("Not a valid Token.") 52 | } 53 | } 54 | 55 | 56 | def dropToken(key:TokenStr)={ 57 | tokensDao.deleteToken(key.tokenStr) 58 | } 59 | 60 | 61 | private def generateToken(key: String) = Token(generateTokenStr, maxTTL, key) 62 | 63 | private def generateTokenStr: TokenStr = TokenStr(UUID.randomUUID().toString) 64 | private def maxTTL = System.currentTimeMillis() + context.tokenTTL 65 | 66 | } 67 | -------------------------------------------------------------------------------- /chapter-4/auth-app/app/utils/Contexts.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.ActorSystem 6 | import play.Application 7 | 8 | @Singleton 9 | class Contexts @Inject()(akkaSystem: ActorSystem, configuration: play.api.Configuration) { 10 | implicit val dbLookup = akkaSystem.dispatchers.lookup("contexts.db-lookups") 11 | implicit val cpuLookup = akkaSystem.dispatchers.lookup("contexts.cpu-operations") 12 | 13 | val tokenTTL = configuration.get[Long]("token.ttl") 14 | } -------------------------------------------------------------------------------- /chapter-4/auth-app/conf/application.conf: -------------------------------------------------------------------------------- 1 | # Default database configuration 2 | slick.dbs.default.driver="slick.driver.H2Driver$" 3 | slick.dbs.default.db.driver="org.h2.Driver" 4 | slick.dbs.default.db.url="jdbc:h2:./auth" 5 | slick.dbs.default.db.user=sa 6 | slick.dbs.default.db.password="" 7 | 8 | #run conf/userdb/1.sql by userdb 9 | //play.applyEvolutions.default=true 10 | play.evolutions.autoApply=true 11 | 12 | evolutionplugin=enabled 13 | play.evolutions.db.default.autoApply=true 14 | play.evolutions.db.default.autoApplyDowns=true 15 | 16 | contexts { 17 | db-lookups{ 18 | throughput = 1 19 | thread-pool-executor { 20 | fixed-pool-size = 10 21 | } 22 | } 23 | 24 | cpu-operations { 25 | fork-join-executor { 26 | parallelism-max = 2 27 | } 28 | } 29 | } 30 | 31 | 32 | token { 33 | ttl = 86400000 # seconds 34 | } 35 | -------------------------------------------------------------------------------- /chapter-4/auth-app/conf/evolutions/default/1.sql: -------------------------------------------------------------------------------- 1 | # --- !Ups 2 | 3 | create table users (email VARCHAR NOT NULL PRIMARY KEY,passwdHash VARCHAR NOT NULL, creationTime BIGINT NOT NULL ); 4 | 5 | create table tokens(key VARCHAR NOT NULL PRIMARY KEY , token VARCHAR NOT NULL UNIQUE , validTill BIGINT NOT NULL); 6 | 7 | --- a user is by default created. password is: test 8 | insert into users (email, passwdHash, creationTime) values ('test@test.com','$2a$11$7M4wUE4VYQBDEHd4eQUbpuiOzl4tB5gZnmQ3t06LNkCbBlwjtRukO',1505070611328); 9 | insert into tokens (key, token, validTill) values ('test@test.com','123-456-789-123',2505070611328); 10 | 11 | # --- !Downs 12 | 13 | drop table users; 14 | drop table tokens; -------------------------------------------------------------------------------- /chapter-4/auth-app/conf/routes: -------------------------------------------------------------------------------- 1 | POST /v1/auth/register auth.UserController.register 2 | POST /v1/auth/login auth.UserController.login 3 | GET /v1/auth/logout/:token auth.UserController.logout(token:String) 4 | 5 | GET /v1/tokens/refresh/:token tokens.TokenController.refreshToken(token:String) 6 | GET /v1/tokens/authenticate/:token tokens.TokenController.authenticate(token:String) -------------------------------------------------------------------------------- /chapter-4/build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | name := "chapter-4" 4 | 5 | version := "1.0" 6 | 7 | lazy val `chapter-4` = (project in file(".")).aggregate( 8 | `web-app`, 9 | `auth-app`, 10 | `so-app`, 11 | `github-app`, 12 | `rank-app`, 13 | commons) 14 | 15 | lazy val commonSettings = Seq( 16 | organization := "com.scalamicroservices", 17 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"), 18 | scalaVersion := "2.12.2", 19 | resolvers ++= Seq("Typesafe Releases" at "http://repo.typesafe.com/typesafe/releases/", 20 | "JBoss" at "https://repository.jboss.org/") 21 | ) 22 | 23 | lazy val commons = BaseProject("commons") 24 | .settings(libraryDependencies ++= Seq(specs2 % Test, playJson)) 25 | 26 | 27 | lazy val `web-app` = PlayProject("web-app") 28 | .settings(libraryDependencies ++= Seq(parserCombinator, ws, specs2 % Test, guice, scalaTest)) 29 | .dependsOn(commons) 30 | 31 | lazy val `so-app` = PlayProject("so-app") 32 | .settings(libraryDependencies ++= Seq(h2, jbcrypt, slick, playSlick, playSlickEvolutions, guice, specs2 % Test)) 33 | .dependsOn(commons) 34 | 35 | lazy val `auth-app` = PlayProject("auth-app") 36 | .settings(libraryDependencies ++= Seq(h2, jbcrypt, slick, playSlick, playSlickEvolutions, guice, specs2 % Test)) 37 | .dependsOn(commons) 38 | 39 | lazy val `rank-app` = PlayProject("rank-app") 40 | .settings(libraryDependencies ++= Seq(guice, ws)) 41 | .dependsOn(commons) 42 | 43 | lazy val `github-app` = PlayProject("github-app") 44 | .settings(libraryDependencies ++= Seq(h2, jbcrypt, slick, playSlick, playSlickEvolutions, guice)) 45 | .dependsOn(commons) 46 | 47 | 48 | def BaseProject(name: String): Project = ( 49 | Project(name, file(name)) 50 | settings (commonSettings: _*) 51 | ) 52 | 53 | def PlayProject(name: String): Project = ( 54 | BaseProject(name) 55 | enablePlugins PlayScala 56 | ) 57 | 58 | val slickV = "3.2.1" 59 | val h2V = "1.4.193" 60 | val playSlickV = "3.0.1" 61 | val jbcryptV = "0.4" 62 | val parserCombinatorV = "1.0.5" 63 | 64 | val slick = "com.typesafe.slick" %% "slick" % slickV 65 | val slickHikariCP = "com.typesafe.slick" %% "slick-hikaricp" % slickV 66 | val h2 = "com.h2database" % "h2" % h2V 67 | val playSlick = "com.typesafe.play" %% "play-slick" % playSlickV 68 | val playSlickEvolutions = "com.typesafe.play" %% "play-slick-evolutions" % playSlickV 69 | val jbcrypt = "org.mindrot" % "jbcrypt" % jbcryptV 70 | val parserCombinator = "org.scala-lang.modules" % "scala-parser-combinators_2.12" % parserCombinatorV 71 | val playJson = "com.typesafe.play" %% "play-json" % "2.6.3" 72 | val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % "test" 73 | val runAll = inputKey[Unit]("Runs all subprojects") 74 | 75 | 76 | runAll := { 77 | (run in Compile in `web-app`).partialInput(" 3000").evaluated 78 | (run in Compile in `so-app`).partialInput(" 5000").evaluated 79 | (run in Compile in `auth-app`).partialInput(" 5001").evaluated 80 | (run in Compile in `rank-app`).partialInput(" 5002").evaluated 81 | } 82 | 83 | fork in run := true 84 | 85 | // enables unlimited amount of resources to be used :-o just for runAll convenience 86 | concurrentRestrictions in Global := Seq( 87 | Tags.customLimit(_ => true) 88 | ) 89 | -------------------------------------------------------------------------------- /chapter-4/commons/src/main/scala/com/microservices/auth/User.scala: -------------------------------------------------------------------------------- 1 | package com.microservices.auth 2 | 3 | import play.api.libs.functional.syntax.unlift 4 | import play.api.libs.json._ 5 | 6 | 7 | case class User(email: String, password: String) 8 | 9 | object User { 10 | implicit val userJS = Json.format[User] 11 | } 12 | 13 | 14 | case class TokenStr(tokenStr: String) 15 | 16 | case class Token(token: TokenStr, validTill: Long, key: String) 17 | 18 | object TokenStr { 19 | implicit val tokenSTRJS = Json.format[TokenStr] 20 | } 21 | 22 | object Token { 23 | implicit val tokenJS = Json.format[Token] 24 | } 25 | 26 | case class SimpleMessage(message: String) 27 | 28 | object SimpleMessage { 29 | implicit val formatSR = Json.format[SimpleMessage] 30 | } 31 | 32 | /** 33 | * In all the service calls we return `com.microservices.auth.ResponseObj` object. It has two sub classes which represent 34 | * a success (with the body) or a failure (with a message). 35 | * 36 | * A sample success call: {"isSuccess":true,"message":{"tokenStr":"677678f7-5dc9-4236-a254-c067b0662e8c"}} 37 | * A sample failure call: {"isSuccess":true,"message":"username/password mismatch"} 38 | * 39 | * The reason we wrap it with `ResponseObj` is that when the json is responded back to the user, the caller can take decision 40 | * based on the `isSuccess` flag. 41 | */ 42 | sealed abstract class ResponseObj(val isSuccess: Boolean) 43 | 44 | case class SuccessRes[T](message: T) extends ResponseObj(true) 45 | 46 | case class FailureRes(message: String) extends ResponseObj(false) 47 | 48 | object SuccessRes { 49 | 50 | import play.api.libs.json._ 51 | import play.api.libs.functional.syntax._ 52 | 53 | implicit def reads[T: Reads]: Reads[SuccessRes[T]] = ( 54 | (JsPath \ "isSuccess").read[Boolean] and 55 | (JsPath \ "message").read[T] 56 | ) ((x, y) => SuccessRes(y)) 57 | 58 | implicit def writes[T: Writes]: Writes[SuccessRes[T]] = ( 59 | (JsPath \ "isSuccess").write[Boolean] and 60 | (JsPath \ "message").write[T] 61 | ) (x => (true, x.message)) 62 | } 63 | 64 | object FailureRes { 65 | 66 | import play.api.libs.json._ 67 | import play.api.libs.functional.syntax._ 68 | 69 | implicit def reads: Reads[FailureRes] = ( 70 | (JsPath \ "isSuccess").read[Boolean] and 71 | (JsPath \ "message").read[String] 72 | ) ((x, y) => FailureRes(y)) 73 | 74 | implicit def writes: Writes[FailureRes] = ( 75 | (JsPath \ "isSuccess").write[Boolean] and 76 | (JsPath \ "message").write[String] 77 | ) (x => (false, x.message)) 78 | } 79 | 80 | object ResponseObj { 81 | def asSuccess[T: Writes](message: T) = Json.toJson(SuccessRes(message)) 82 | def asFailure(message: String) = Json.toJson(FailureRes(message)) 83 | 84 | implicit def reads[T:Reads] = new Reads[ResponseObj] { 85 | override def reads(json: JsValue) = 86 | if(json.validate(SuccessRes.reads[T]).isSuccess) 87 | json.validate(SuccessRes.reads[T]) 88 | else if(json.validate(FailureRes.reads).isSuccess) { 89 | json.validate(FailureRes.reads) 90 | } 91 | else { 92 | throw new RuntimeException("no valid reads found: "+json+". reads called with: "+implicitly[Reads[T]]+". ") 93 | } 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /chapter-4/commons/src/main/scala/com/microservices/search/Search.scala: -------------------------------------------------------------------------------- 1 | package com.microservices.search 2 | 3 | import play.api.libs.json.Json 4 | 5 | abstract class SUserResult{ 6 | require(score >= 0 && score <= 1, s"score must be in range of [0-1]. passed: $score") 7 | 8 | /** 9 | * value in range of 0-1 absolute 10 | */ 11 | def score: Float 12 | def tag: String 13 | def location: String 14 | } 15 | 16 | case class SearchFilter(location: Option[String], tag: Option[String]) 17 | object SearchFilter{ 18 | implicit val format = Json.format[SearchFilter] 19 | } 20 | 21 | case class SOUser(id:Long, name:String, soAccountId: Long, aboutMe:String, soLink:String="#", location:String) 22 | object SOUser{ 23 | implicit val soUserJSON = Json.format[SOUser] 24 | } 25 | 26 | case class SOTag(id:Int, name:String) 27 | 28 | object SOTag{ 29 | implicit val soTagJSON = Json.format[SOTag] 30 | } 31 | 32 | 33 | case class SoUserScore(user:SOUser, map: Map[SOTag, Int]) 34 | 35 | case class SOSearchResult(override val score:Float, soTag: SOTag, soUser: SOUser) extends SUserResult { 36 | override val location = soUser.location 37 | override val tag = soTag.name 38 | } 39 | 40 | object SOSearchResult{ 41 | implicit val soSearchResultJSON = Json.format[SOSearchResult] 42 | } 43 | 44 | -------------------------------------------------------------------------------- /chapter-4/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 -------------------------------------------------------------------------------- /chapter-4/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" 4 | 5 | resolvers += "rediscala" at "http://dl.bintray.com/etaty/maven" 6 | 7 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") 8 | 9 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") 10 | 11 | 12 | -------------------------------------------------------------------------------- /chapter-4/rank-app/app/controller/RankController.scala: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import javax.inject.Inject 4 | 5 | import com.google.common.base.Throwables 6 | import com.microservices.auth.{FailureRes, ResponseObj, SuccessRes} 7 | import com.microservices.search.{SOSearchResult, SearchFilter} 8 | import play.api.Logger 9 | import play.api.libs.ws.WSClient 10 | import play.api.mvc.{AbstractController, ControllerComponents} 11 | import utils.RankProperties 12 | 13 | import scala.concurrent.{ExecutionContext, Future} 14 | 15 | class RankController @Inject()(cc: ControllerComponents, urls: RankProperties, ws: WSClient)(implicit val exec: ExecutionContext) 16 | extends AbstractController(cc) { 17 | 18 | def search(location: Option[String], tag: Option[String]) = Action.async { implicit request => 19 | getResultsForQuery(SearchFilter(location, tag)).map(x => Ok(ResponseObj.asSuccess(x))).recoverWith { 20 | case e: Exception => Future.successful(BadRequest(ResponseObj.asFailure(e.getMessage))) 21 | } 22 | } 23 | 24 | def searchByQuery = Action.async(parse.json) { implicit request => 25 | val ans = getResultsForQuery(SearchFilter.format.reads(request.body).get).map(x => Ok(ResponseObj.asSuccess(x))) 26 | ans.recoverWith { 27 | case e: Exception => Future.successful(BadRequest(ResponseObj.asFailure("stackoverflow call failed: "+Throwables.getStackTraceAsString(e)))) 28 | } 29 | } 30 | 31 | private def getResultsForQuery(q: SearchFilter): Future[Seq[SOSearchResult]] = { 32 | ws.url(urls.stackoverflowURL+"so/v1/search") 33 | .addHttpHeaders("Accept" -> "application/json") 34 | .post(SearchFilter.format.writes(q)) 35 | .map(x => { 36 | Logger.info(x.body) 37 | x.json.as[Seq[SOSearchResult]] match { 38 | case ans:Seq[_] => ans.asInstanceOf[Seq[SOSearchResult]] 39 | case e => 40 | Logger.info("Unknown format response from stackoverflow "+e) 41 | throw new RuntimeException("Unknown format response from stackoverflow "+e) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /chapter-4/rank-app/app/utils/RankProperties.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.Inject 4 | 5 | 6 | class RankProperties @Inject()(configuration: play.api.Configuration) { 7 | 8 | if (configuration.get[String]("url.platform.stackoverflow") == null) { 9 | throw new IllegalStateException("stackoverflow url not configured") 10 | } 11 | if (configuration.get[String]("url.platform.auth") == null) { 12 | throw new IllegalStateException("auth url not configured") 13 | } 14 | 15 | val stackoverflowURL: String = configuration.get[String]("url.platform.stackoverflow") 16 | val githubURL: String = configuration.get[String]("url.platform.github") 17 | val authURL: String = configuration.get[String]("url.platform.auth") 18 | 19 | def getAllPlatforms = List(stackoverflowURL, githubURL) 20 | } 21 | -------------------------------------------------------------------------------- /chapter-4/rank-app/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.filters.enabled=[] 2 | 3 | url.platform.stackoverflow="http://localhost:5000/" 4 | url.platform.auth="http://localhost:5001/" 5 | url.platform.github="http://localhost:5002/" -------------------------------------------------------------------------------- /chapter-4/rank-app/conf/routes: -------------------------------------------------------------------------------- 1 | GET /api/v1/search controller.RankController.search(location: Option[String], tag: Option[String]) 2 | POST /api/v1/search controller.RankController.searchByQuery -------------------------------------------------------------------------------- /chapter-4/so-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM anapsix/alpine-java 2 | 3 | MAINTAINER Selvam Palanimalai 4 | 5 | 6 | COPY target/universal/so-app-0.1-SNAPSHOT.zip /opt/scala/so-app.zip 7 | 8 | RUN unzip /opt/scala/so-app.zip -d /opt/scala/ 9 | 10 | CMD /opt/scala/so-app-0.1-SNAPSHOT/bin/so-app -Dplay.http.secret.key=$HTTP_SECRET -Dplay.crypto.secret=$APP_SECRET 11 | -------------------------------------------------------------------------------- /chapter-4/so-app/app/controllers/SOSearchController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.search.{SOSearchResult, SearchFilter} 6 | import play.api.libs.json.Json 7 | import play.api.mvc._ 8 | import service.SearchService 9 | import users.Contexts 10 | 11 | import scala.collection.immutable.Seq 12 | import scala.concurrent.Future 13 | 14 | @Singleton 15 | class SOSearchController @Inject()(service: SearchService, context: Contexts, cc: ControllerComponents) extends AbstractController(cc) { 16 | 17 | import context.cpuLookup 18 | 19 | def searchPost = Action.async(parse.json) { implicit request => 20 | val body = request.body 21 | val loc = (body \ "location").validate[String] 22 | val tag = (body \ "tag").validate[String] 23 | if (loc.isError || tag.isError) { 24 | Future.successful(BadRequest(s"Not a valid input: $body")) 25 | } else { 26 | val filter = SearchFilter(Option(loc.get), Option(tag.get)) 27 | search(filter).map(x => Ok(Json.toJson(x))) 28 | } 29 | } 30 | 31 | // def searchPost = Action.async(parse.json) { implicit request => 32 | // val body = request.body 33 | // val ans = body.validate[SearchFilter] match { 34 | // case s: JsSuccess[SearchFilter] => 35 | // val filter: SearchFilter = s.get 36 | // search(SearchFilter(filter.city, filter.tags)).map(x => Ok(ResponseObj.asSuccess(x))) 37 | // case s: JsError => Future.successful(BadRequest(s"Not a valid input: $body")) 38 | // } 39 | // ans 40 | // } 41 | 42 | def searchGet(location: String, tag: String) = Action.async { implicit request => 43 | search(SearchFilter(Option(location), Option(tag))).map(x => Ok(Json.toJson(x))) 44 | } 45 | 46 | private def search(filter: SearchFilter): Future[Seq[SOSearchResult]] = { 47 | service.searchFlatten(filter) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /chapter-4/so-app/app/dao/SearchDao.scala: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import javax.inject.Singleton 4 | 5 | import com.google.inject.Inject 6 | import com.microservices.search.{SOTag, SOUser, SoUserScore} 7 | import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} 8 | import slick.jdbc.JdbcProfile 9 | import slick.jdbc.GetResult 10 | 11 | import scala.collection.immutable.Iterable 12 | import scala.concurrent.duration.Duration 13 | import scala.concurrent.{Await, ExecutionContext, Future} 14 | 15 | 16 | @Singleton 17 | class SearchDao @Inject()(protected val dbConfigProvider: DatabaseConfigProvider) extends HasDatabaseConfigProvider[JdbcProfile] { 18 | 19 | import profile.api._ 20 | 21 | implicit val getUserResult: GetResult[(SOUser, SOTag, Int)] = GetResult(r => 22 | (SOUser(r.nextInt(), r.nextString(), r.nextInt(), r.nextString(), r.nextString(), r.nextString()), 23 | SOTag(r.nextInt(), r.nextString), 24 | r.nextInt())) 25 | 26 | 27 | def getUsers(location: Option[String], tag: Option[String])(implicit exec:ExecutionContext): Future[Iterable[SoUserScore]] = { 28 | 29 | val selectQ = 30 | """select a.id,a.name, a.so_account_id, a.about_me, a.so_link, a.location, c.id,c.name,b.points from so_user_info a 31 | join so_reputation b on b.user=a.id 32 | join so_tag c on b.tag=c.id 33 | where 1=1 """ 34 | 35 | val allFuture = (location, tag) match { 36 | case (Some(loc), Some(t)) => 37 | db.run(sql"""#$selectQ 38 | AND a.location = LOWER($loc) 39 | AND c.name = LOWER ($t)""".as[(SOUser, SOTag, Int)]) 40 | case (Some(loc), None) => 41 | db.run(sql"""#$selectQ 42 | AND a.location = LOWER(${loc})""".as[(SOUser, SOTag, Int)]) 43 | case (None, Some(t)) => 44 | db.run(sql"""#$selectQ 45 | AND UPPER c.name = LOWER (${t})""".as[(SOUser, SOTag, Int)]) 46 | case (None, None) => db.run(sql"""#$selectQ""".as[(SOUser, SOTag, Int)]) 47 | } 48 | 49 | allFuture.map(allUsers => { 50 | allUsers.groupBy(x => x._1).map(pair => { 51 | SoUserScore(pair._1, pair._2.map(x => (x._2, x._3)).toMap) 52 | }) 53 | }) 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /chapter-4/so-app/app/service/SearchService.scala: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.search.{SOSearchResult, SearchFilter, SoUserScore} 6 | import dao.SearchDao 7 | import play.api.Logger 8 | 9 | import scala.collection.immutable.{Iterable, Seq} 10 | import scala.concurrent.{ExecutionContext, Future} 11 | 12 | @Singleton 13 | class SearchService @Inject()(dao: SearchDao) { 14 | 15 | private val log = Logger(getClass) 16 | 17 | def search(filter: SearchFilter)(implicit exec: ExecutionContext): Future[Iterable[SoUserScore]] = { 18 | dao.getUsers(filter.location, filter.tag) 19 | } 20 | 21 | def searchFlatten(filter: SearchFilter)(implicit exec: ExecutionContext): Future[Seq[SOSearchResult]] = { 22 | log.debug(s"Request for filter: $filter") 23 | 24 | search(filter).map(ans => { 25 | ans.toList.flatMap(x => x.map.map(tags => SOSearchResult(1, tags._1, x.user))) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter-4/so-app/app/users/Contexts.scala: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | 4 | import javax.inject.{Inject, Singleton} 5 | 6 | import akka.actor.ActorSystem 7 | import play.Application 8 | 9 | @Singleton 10 | class Contexts @Inject()(akkaSystem: ActorSystem, configuration: play.api.Configuration) { 11 | implicit val dbLookup = akkaSystem.dispatchers.lookup("contexts.db-lookups") 12 | implicit val cpuLookup = akkaSystem.dispatchers.lookup("contexts.cpu-operations") 13 | } -------------------------------------------------------------------------------- /chapter-4/so-app/conf/application.conf: -------------------------------------------------------------------------------- 1 | # Default database configuration 2 | slick.dbs.default.driver="slick.driver.H2Driver$" 3 | slick.dbs.default.db.driver="org.h2.Driver" 4 | slick.dbs.default.db.url="jdbc:h2:./so" 5 | slick.dbs.default.db.user=sa 6 | slick.dbs.default.db.password="" 7 | 8 | #run conf/userdb/1.sql by userdb 9 | #play.applyEvolutions.default=true 10 | play.evolutions.autoApply=true 11 | 12 | evolutionplugin=enabled 13 | play.evolutions.db.default.autoApply=true 14 | play.evolutions.db.default.autoApplyDowns=true 15 | 16 | contexts { 17 | db-lookups{ 18 | throughput = 1 19 | thread-pool-executor { 20 | fixed-pool-size = 10 21 | } 22 | } 23 | 24 | cpu-operations { 25 | fork-join-executor { 26 | parallelism-max = 2 27 | } 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /chapter-4/so-app/conf/routes: -------------------------------------------------------------------------------- 1 | POST /so/v1/search controllers.SOSearchController.searchPost 2 | GET /so/v1/search controllers.SOSearchController.searchGet(location:String, tag:String) -------------------------------------------------------------------------------- /chapter-4/web-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM anapsix/alpine-java 2 | 3 | MAINTAINER Selvam Palanimalai 4 | 5 | WORKDIR /opt/scala/ 6 | 7 | COPY target/universal/web-app-0.1-SNAPSHOT.zip web-app.zip 8 | 9 | RUN unzip web-app.zip && rm web-app-0.1-SNAPSHOT/conf/application.conf 10 | 11 | CMD ln -s /opt/cmaps/application.conf web-app-0.1-SNAPSHOT/conf/application.conf && /opt/scala/web-app-0.1-SNAPSHOT/bin/web-app -Dplay.http.secret.key=$HTTP_SECRET -Dplay.crypto.secret=$APP_SECRET 12 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/app-start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import AppRoutes from './components/AppRoutes'; 6 | 7 | window.onload = () => { 8 | ReactDOM.render(, document.getElementById('main')); 9 | }; 10 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/AppRoutes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Router, browserHistory } from 'react-router'; 5 | import routes from '../routes'; 6 | 7 | export default class AppRoutes extends React.Component { 8 | render() { 9 | return ( 10 | window.scrollTo(0, 0)}/> 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/DashboardPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router'; 5 | import NotFoundPage from './NotFoundPage'; 6 | import SearchBar from './SearchBar'; 7 | import SearchResults from './SearchResults'; 8 | import { Loader, Input, Button} from 'semantic-ui-react'; 9 | 10 | 11 | export default class DashboardPage extends React.Component { 12 | constructor(){ 13 | super(); 14 | this.hostname = window.location.protocol + "//" + window.location.hostname + ":"+(window.location.port); 15 | this.state = { 16 | loggedIn : null, 17 | results: [], 18 | query: null, 19 | loading : false 20 | } 21 | } 22 | handleSearchClick(){ 23 | let self = this; 24 | this.setState({ 25 | loading: true 26 | }); 27 | return fetch(`${this.hostname}/api/v1/searchQuery?query=${this.state.query}`,{ 28 | credentials:"same-origin" 29 | }) 30 | .then(function(response) { 31 | if(response.status==200){ 32 | return response.json() 33 | } else { 34 | throw new Error(response.status) 35 | } 36 | 37 | }).then(function(body) { 38 | self.setState({ 39 | loading: false, 40 | results : body.message 41 | }) 42 | }).catch(function(err){ 43 | self.setState({ 44 | loading: false, 45 | results:[] 46 | }) 47 | }) 48 | } 49 | componentDidMount(){ 50 | let self = this; 51 | 52 | return fetch(this.hostname+'/api/status',{ 53 | credentials:"same-origin" 54 | }) 55 | .then(function(response) { 56 | if(response.status==200){ 57 | return response.json() 58 | } else { 59 | throw new Error(response.status) 60 | } 61 | 62 | }).then(function(body) { 63 | self.setState({ 64 | loggedIn : true 65 | }) 66 | }).catch(function(err){ 67 | console.error(err) 68 | self.setState({ 69 | loggedIn :false 70 | }) 71 | }) 72 | } 73 | render() { 74 | let content; 75 | if(this.state.loggedIn == null){ 76 | content = 77 | } else if(this.state.loggedIn == true) { 78 | // Dashboard view 79 | content =
80 |
81 | this.setState({query: e.target.value }) } 83 | size="massive" 84 | ref="query" 85 | placeholder='scala developers in singapore...' 86 | /> 87 | 88 |
89 | 90 |
91 | } else { 92 | // Redirect user to homepage 93 | window.location.href=this.hostname 94 | } 95 | return ( 96 |
97 | {content} 98 |
99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/Flag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | const data = { 6 | 'cu': { 7 | 'name': 'Cuba', 8 | 'icon': 'flag-cu.png', 9 | }, 10 | 'fr': { 11 | 'name': 'France', 12 | 'icon': 'flag-fr.png', 13 | }, 14 | 'jp': { 15 | 'name': 'Japan', 16 | 'icon': 'flag-jp.png', 17 | }, 18 | 'nl': { 19 | 'name': 'Netherlands', 20 | 'icon': 'flag-nl.png', 21 | }, 22 | 'uz': { 23 | 'name': 'Uzbekistan', 24 | 'icon': 'flag-uz.png', 25 | } 26 | }; 27 | 28 | export default class Flag extends React.Component { 29 | render() { 30 | const name = data[this.props.code].name; 31 | const icon = data[this.props.code].icon; 32 | return ( 33 | 34 | 35 | {this.props.showName && {name}} 36 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/IndexPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {Form, Button, Input, Header, Message} from 'semantic-ui-react'; 5 | import queryString from 'query-string'; 6 | 7 | const loginForm = { 8 | width: '300px', 9 | margin: "auto", 10 | 11 | }; 12 | export default class IndexPage extends React.Component { 13 | constructor(){ 14 | super(); 15 | this.hostname = window.location.protocol + "//" + window.location.hostname + ":"+(window.location.port); 16 | this.state = { 17 | loading : '', 18 | loggedIn:null, 19 | flashMessage: "", 20 | email:null, 21 | password:null 22 | } 23 | } 24 | 25 | componentDidMount(){ 26 | let self = this; 27 | return fetch(this.hostname+'/api/status',{ 28 | credentials:"same-origin" 29 | }) 30 | .then(function(response) { 31 | if(response.status==200){ 32 | return response.json() 33 | } else { 34 | throw new Error(response.status) 35 | } 36 | }).then(function(body) { 37 | self.setState({ 38 | loggedIn : true 39 | }) 40 | }).catch(function(err){ 41 | self.setState({ 42 | loggedIn :false 43 | }) 44 | }) 45 | } 46 | 47 | render() { 48 | if(this.state.loggedIn){ 49 | window.location.href= this.hostname+'/dashboard' 50 | } 51 | let message = "" 52 | if( this.state.flashMessage != ''){ 53 | message = 58 | } 59 | return ( 60 |
61 |
62 | {message} 63 |
Log In
64 |
New User? Register here.
65 |
66 | 67 |
68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 |
76 |
77 | ); 78 | } 79 | 80 | updateEmailState(e){ 81 | this.setState({email:e.target.value}); 82 | } 83 | 84 | updatePasswordState(e){ 85 | this.setState({password:e.target.value}); 86 | } 87 | 88 | sendLoginRequest(){ 89 | let self = this; 90 | this.setState({ 91 | loading:'loading' 92 | }); 93 | return fetch(this.hostname+"/api/login",{ 94 | method:'POST', 95 | credentials:'same-origin', 96 | headers: {'Content-Type':'application/x-www-form-urlencoded'}, // this line is important, if this content-type is not set it wont work 97 | body: queryString.stringify({ 98 | email:this.state.email, 99 | password:this.state.password 100 | }) 101 | }) 102 | .then(function(response) { 103 | if(response.status == 200){ 104 | return response.json() 105 | } 106 | self.setState({ 107 | loggedIn:false, 108 | loading: false, 109 | flashMessage: "Login failed with response code: "+response.status 110 | }); 111 | throw new Error(response.status) 112 | }).then(function(responseObj){ 113 | window.location.href=self.hostname+"/dashboard?tokenStr="+responseObj.message.tokenStr; 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/Medal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | const typeMap = { 6 | 'G': 'Gold', 7 | 'S': 'Silver', 8 | 'B': 'Bronze' 9 | }; 10 | 11 | export default class Medal extends React.Component { 12 | render() { 13 | return ( 14 |
  • 15 | {this.props.type} 16 | {this.props.year} 17 | {this.props.city} 18 | ({this.props.event}) 19 | {this.props.category} 20 |
  • 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router'; 5 | 6 | export default class NotFoundPage extends React.Component { 7 | render() { 8 | return ( 9 |
    10 |

    404

    11 |

    Page not found!

    12 |

    13 | Go back to the main page 14 |

    15 |
    16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/RegisterPage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {Form, Button, Input, Header, Message} from 'semantic-ui-react'; 5 | import queryString from 'query-string'; 6 | 7 | const loginForm = { 8 | width: '300px', 9 | margin: "auto", 10 | 11 | }; 12 | export default class RegisterPage extends React.Component { 13 | constructor(){ 14 | super(); 15 | this.hostname = window.location.protocol + "//" + window.location.hostname + ":"+(window.location.port); 16 | this.state = { 17 | loading: false, 18 | loggedIn: false, 19 | email: null, 20 | password: null, 21 | flashMessage: '' 22 | } 23 | } 24 | 25 | componentDidMount(){ 26 | let self = this; 27 | return fetch(this.hostname+'/api/status',{ 28 | credentials:"same-origin" 29 | }) 30 | .then(function(response) { 31 | if(response.status==200){ 32 | return response.json() 33 | } else { 34 | throw new Error(response.status) 35 | } 36 | }).then(function(body) { 37 | self.setState({ 38 | loggedIn: true 39 | }) 40 | }).catch(function(err){ 41 | self.setState({ 42 | loggedIn: false 43 | }) 44 | }) 45 | } 46 | 47 | render() { 48 | if(this.state.loggedIn){ 49 | window.location.href= this.hostname+'/dashboard' 50 | } 51 | let message = "" 52 | if( this.state.flashMessage != ''){ 53 | message = 58 | } 59 | return ( 60 |
    61 |
    62 | {message} 63 |
    Register new Account
    64 |
    Old User? Login here.
    65 |
    66 | 67 |
    68 | 69 | 70 | 71 | 72 | 73 |
    74 | 75 |
    76 |
    77 | ); 78 | } 79 | 80 | updateEmailState(e){ 81 | this.setState({email:e.target.value}); 82 | } 83 | 84 | updatePasswordState(e){ 85 | this.setState({password:e.target.value}); 86 | } 87 | 88 | sendRegisterRequest(){ 89 | let self = this; 90 | this.setState({ 91 | loading: true 92 | }); 93 | return fetch(this.hostname+"/api/register",{ 94 | method:'POST', 95 | credentials:'same-origin', 96 | headers: {'Content-Type':'application/x-www-form-urlencoded'}, // this line is important, if this content-type is not set it wont work 97 | body: queryString.stringify({ 98 | email:this.state.email, 99 | password:this.state.password 100 | }) 101 | }) 102 | .then(function(response) { 103 | if(response.status == 200){ 104 | return response.json() 105 | } 106 | self.setState({ 107 | loggedIn: false, 108 | loading: false, 109 | flashMessage: "Registration failed with response code: "+response.status 110 | }); 111 | throw new Error(response.status) 112 | }).then(function(responseObj){ 113 | window.location.href=self.hostname+"/dashboard?tokenStr="+responseObj.message.tokenStr; 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/SearchBar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router'; 5 | import {Input, Button} from 'semantic-ui-react'; 6 | export default class SearchBar extends React.Component { 7 | render() { 8 | return ( 9 |
    10 | 14 | 18 | 19 |
    20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/components/SearchResults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router'; 5 | import {List, Image, Message, Icon} from 'semantic-ui-react'; 6 | export default class SearchResults extends React.Component { 7 | 8 | renderNotFoundView(){ 9 | return ( 10 | 11 | 12 | 13 | No results found. 14 | Try searching 'scala developers in singapore' 15 | 16 | 17 | ) 18 | 19 | } 20 | 21 | renderLoadingView(){ 22 | return ( 23 | 24 | 25 | 26 | Just one second 27 | Loading results.... 28 | 29 | 30 | ) 31 | } 32 | 33 | /* 34 | soTag: 35 | {id: 1, name: "scala"} 36 | soUser: 37 | {id: 2, name: "Muhammad", soAccountId: 2, aboutMe: "Toy apps or cute things like qsort in haskell really give the wrong idea.", soLink: "#",... } 38 | */ 39 | renderRows(results){ 40 | let content = [] 41 | results.forEach( row =>{ 42 | content.push( 43 | 44 | 45 | 46 | {row.soUser.name} 47 | Lives in {row.soUser.location} 48 | 49 | 50 | ) 51 | }) 52 | return content 53 | } 54 | 55 | render() { 56 | // Loading View 57 | if( this.props.loading) { 58 | return this.renderLoadingView() 59 | } 60 | if(Array.isArray(this.props.results) && this.props.results.length > 0){ 61 | return( 62 | {this.renderRows(this.props.results)} 63 | ) 64 | } 65 | 66 | // No results found view. 67 | if(Array.isArray(this.props.results) && this.props.results.length == 0){ 68 | return this.renderNotFoundView() 69 | } 70 | 71 | // Default View. 72 | return this.renderNotFoundView() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-4-ui", 3 | "version": "1.0.0", 4 | "description": "Simple example application for scala microservices book", 5 | "main": "src/server.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node_modules/.bin/babel-node --presets react,es2015 src/server.js", 8 | "start-dev": "npm run start-dev-hmr", 9 | "start-dev-single-page": "node_modules/.bin/http-server src/static", 10 | "start-dev-hmr": "node_modules/.bin/webpack-dev-server --progress --inline --hot --open", 11 | "build": "cross-env NODE_ENV=development node_modules/.bin/webpack -p" 12 | }, 13 | "author": "Selvam Palanimalai", 14 | "license": "MIT", 15 | "dependencies": { 16 | "babel-cli": "^6.11.4", 17 | "babel-core": "^6.13.2", 18 | "babel-loader": "^6.2.5", 19 | "babel-plugin-react-html-attrs": "^2.0.0", 20 | "babel-preset-es2015": "^6.13.2", 21 | "babel-preset-react": "^6.11.1", 22 | "babel-preset-react-hmre": "^1.1.1", 23 | "body-parser": "^1.16.1", 24 | "cross-env": "^3.1.4", 25 | "ejs": "^2.5.1", 26 | "express": "^4.14.0", 27 | "express-session": "^1.15.1", 28 | "query-string": "^4.3.2", 29 | "react": "^15.3.1", 30 | "react-dom": "^15.3.1", 31 | "react-router": "^2.6.1", 32 | "request": "^2.79.0", 33 | "semantic-ui-react": "^0.66.0" 34 | }, 35 | "devDependencies": { 36 | "http-server": "^0.9.0", 37 | "react-hot-loader": "^1.3.0", 38 | "webpack": "^1.13.2", 39 | "webpack-dev-server": "^1.14.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import { Route, IndexRoute } from 'react-router'; 5 | import Layout from './components/Layout'; 6 | import IndexPage from './components/IndexPage'; 7 | import RegisterPage from './components/RegisterPage'; 8 | import DashboardPage from './components/DashboardPage'; 9 | import NotFoundPage from './components/NotFoundPage'; 10 | 11 | const routes = ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default routes; 21 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/assets/javascripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = process.env.NODE_ENV !== "production"; 4 | 5 | const webpack = require('webpack'); 6 | const path = require('path'); 7 | 8 | module.exports = { 9 | devtool: debug ? 'inline-sourcemap' : null, 10 | entry: path.join(__dirname, 'app-start.js'), 11 | devServer: { 12 | inline: true, 13 | port: 3333, 14 | contentBase: "/static/", 15 | historyApiFallback: { 16 | index: '/index-static.html' 17 | } 18 | }, 19 | output: { 20 | path: "../../../public/js/",//path.join(__dirname, 'static', 'js'), 21 | publicPath: "/js/", 22 | filename: 'bundle.js' 23 | }, 24 | module: { 25 | loaders: [{ 26 | test: path.join(__dirname), 27 | loader: ['babel-loader'], 28 | query: { 29 | cacheDirectory: 'babel_cache', 30 | presets: debug ? ['react', 'es2015'] : ['react', 'es2015'] 31 | } 32 | }] 33 | }, 34 | plugins: debug ? [] : [ 35 | new webpack.DefinePlugin({ 36 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 37 | }), 38 | new webpack.optimize.DedupePlugin(), 39 | new webpack.optimize.OccurenceOrderPlugin(), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { warnings: false }, 42 | mangle: true, 43 | sourcemap: false, 44 | beautify: false, 45 | dead_code: true 46 | }), 47 | ] 48 | }; 49 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/controller/LoginController.scala: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth._ 6 | import play.api.data.Forms._ 7 | import play.api.data._ 8 | import play.api.libs.json.{JsError, JsSuccess} 9 | import play.api.libs.ws.JsonBodyWritables._ 10 | import play.api.libs.ws.WSClient 11 | import play.api.mvc._ 12 | import utils.AllProperties 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | @Singleton 17 | class LoginController @Inject()(cc: ControllerComponents, config: AllProperties, ws: WSClient, security: SecurityAction)(implicit val ec: ExecutionContext) extends AbstractController(cc) { 18 | 19 | def status = security { request => 20 | Ok(ResponseObj.asSuccess("success")) 21 | } 22 | 23 | case class UserData(email: String, password: String) 24 | 25 | val userForm = Form( 26 | mapping( 27 | "email" -> text, 28 | "password" -> text 29 | )(UserData.apply)(UserData.unapply) 30 | ) 31 | 32 | def login = Action.async(parse.form(userForm)) { implicit request => 33 | val body = request.body 34 | ws.url(config.authURL + "v1/auth/login") 35 | .addHttpHeaders("Accept" -> "application/json") 36 | .post(User.userJS.writes(User(body.email, body.password))) 37 | .map { 38 | response => 39 | if(response.status != 200){ 40 | Unauthorized(ResponseObj.asFailure("authentication failure: " + response.body)) 41 | } else { 42 | response.json.validate[TokenStr] match { 43 | case s: JsSuccess[TokenStr] => 44 | val token = s.get 45 | Ok(ResponseObj.asSuccess(token)) 46 | .withSession("token" -> token.tokenStr) 47 | case e: JsError => Unauthorized(ResponseObj.asFailure("authentication failure")) 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | def register = Action.async(parse.form(userForm)) { implicit request => 55 | val body = request.body 56 | ws.url(config.authURL + "v1/auth/register") 57 | .addHttpHeaders("Accept" -> "application/json") 58 | .post(User.userJS.writes(User(body.email, body.password))) 59 | .map { 60 | response => 61 | if(response.status != 200){ 62 | Unauthorized(ResponseObj.asFailure("could not register user: " + response.body)) 63 | } else { 64 | response.json.validate[TokenStr] match { 65 | case s: JsSuccess[TokenStr] => 66 | val token = s.get 67 | Ok(ResponseObj.asSuccess(token)) 68 | .withSession("token" -> token.tokenStr) 69 | case e: JsError => Unauthorized(ResponseObj.asFailure("could not register user")) 70 | } 71 | } 72 | } 73 | } 74 | 75 | 76 | def logout = Action { 77 | // Delete session entry for request SID 78 | Ok(ResponseObj.asSuccess("success")).withNewSession 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/controller/SearchController.scala: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import com.microservices.auth.ResponseObj 6 | import com.microservices.search.SearchFilter 7 | import parser.QueryParser 8 | import play.api.libs.ws.WSClient 9 | import play.api.mvc.{AbstractController, ControllerComponents} 10 | import utils.AllProperties 11 | 12 | import scala.concurrent.{ExecutionContext, Future} 13 | 14 | 15 | @Singleton 16 | class SearchController @Inject()(securityAction: SecurityAction, cc: ControllerComponents, config: AllProperties, ws: WSClient)(implicit val exec: ExecutionContext) 17 | extends AbstractController(cc) { 18 | 19 | /** 20 | * The search syntax will be of the form: 21 | * 22 | * `(scala, abcd) opt(developers) in (city)` 23 | * 24 | * @return 25 | */ 26 | def search(location: Option[String], tag: Option[String]) = securityAction.async { implicit request => 27 | 28 | /** 29 | * Get DNS for so-app service. This can come from a file.properties. k8s can load the file via a configMap. 30 | */ 31 | getResultsForQuery(SearchFilter(location, tag)).map(Ok(_)) 32 | } 33 | 34 | 35 | def searchQuery(query: String) = Action.async { implicit request => 36 | QueryParser.parse(query) match { 37 | case Left(err) => 38 | Future(Ok(ResponseObj.asFailure("could not parse query. error: " + query))) 39 | case Right(searchFilter) => getResultsForQuery(searchFilter) 40 | .map(x => Ok(x)) 41 | .recoverWith { 42 | case e: Exception => Future.successful(BadRequest(ResponseObj.asFailure(e.getMessage))) 43 | } 44 | } 45 | } 46 | 47 | private def getResultsForQuery(q: SearchFilter): Future[String] = { 48 | ws.url(config.rankURL + "api/v1/search") 49 | .addHttpHeaders("Accept" -> "application/json") 50 | .post(SearchFilter.format.writes(q)) 51 | .map(x => { 52 | x.body 53 | }) 54 | } 55 | } -------------------------------------------------------------------------------- /chapter-4/web-app/app/controller/SecurityAction.scala: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import javax.inject.Inject 4 | 5 | import com.microservices.auth.ResponseObj 6 | import play.api.Logger 7 | import play.api.libs.ws.WSClient 8 | import play.api.mvc._ 9 | import utils.AllProperties 10 | 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | /** 14 | * This class does authentication based on the session. 15 | */ 16 | class SecurityAction @Inject()(parser: BodyParsers.Default, config: AllProperties, ws: WSClient) 17 | (implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) { 18 | override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = { 19 | Logger.info("Calling action") 20 | request.session.get("token") match { 21 | case Some(token) => 22 | ws.url(config.authURL + "v1/tokens/authenticate/" + token) 23 | .get() 24 | .flatMap(x => { 25 | Logger.info("Auth successful.. " + x) 26 | block(request) 27 | }) 28 | .recoverWith { 29 | case e: Exception => 30 | Future.successful(Results.BadRequest(ResponseObj.asFailure(e.getMessage))) 31 | } 32 | case None => 33 | Logger.info("Not logged in. Please login") 34 | Future.successful(Results.BadRequest(ResponseObj.asFailure(s"Not logged in. Please login"))) 35 | // block(request) 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /chapter-4/web-app/app/parser/QueryParser.scala: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import com.microservices.search.SearchFilter 4 | 5 | import scala.util.parsing.combinator.RegexParsers 6 | 7 | 8 | object QueryParser { 9 | 10 | /* 11 | Scala in london 12 | Java developers in new york 13 | Javascript developers in San Jose 14 | */ 15 | 16 | /** 17 | * Returns a either whose left would be failure cause. and right would be the result 18 | * 19 | * @param query 20 | * @return 21 | */ 22 | def parse(query: String): Either[String, SearchFilter] = { 23 | SearchParser(query) 24 | } 25 | 26 | 27 | object SearchParser extends RegexParsers { 28 | 29 | private def tag = "[^\\s]+".r ^^ (x => x) 30 | 31 | // private def in = """(?i)\Qin\E""".r 32 | private def in = 33 | """(?i)i(?i)n""".r 34 | 35 | // private def developer = """(?i)\Qdevelopers?\E""".r 36 | //(?i) is to ignore the case of first character (d or D) 37 | private def developer = 38 | """(?i)d(?i)e(?i)v(?i)e(?i)l(?i)o(?i)p(?i)e(?i)r(?i)s?""".r 39 | 40 | private def city = ".+".r ^^ (name => name) 41 | 42 | private def expr = (((opt(tag) <~ opt(developer)) <~ opt(in)) ~ opt(city)) ^^ (x => 43 | SearchFilter(x._2, x._1.filter(x => !x.toLowerCase().startsWith("developer")))) 44 | 45 | def apply(st: String): Either[String, SearchFilter] = parseAll(expr, st) match { 46 | case Success(ob, _) => Right(ob) 47 | case NoSuccess(msg, _) => Left(msg) 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /chapter-4/web-app/app/utils/AllProperties.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.Inject 4 | 5 | class AllProperties @Inject()(configuration: play.api.Configuration) { 6 | if(configuration.get[String]("url.platform.rank") == null){ 7 | throw new IllegalStateException("rank url not configured") 8 | } 9 | if(configuration.get[String]("url.platform.auth") == null){ 10 | throw new IllegalStateException("auth url not configured") 11 | } 12 | 13 | val rankURL: String = configuration.get[String]("url.platform.rank") 14 | val authURL: String = configuration.get[String]("url.platform.auth") 15 | } 16 | -------------------------------------------------------------------------------- /chapter-4/web-app/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.filters.enabled=[] 2 | 3 | url.platform.rank="http://localhost:5002/" 4 | url.platform.auth="http://localhost:5001/" -------------------------------------------------------------------------------- /chapter-4/web-app/conf/routes: -------------------------------------------------------------------------------- 1 | GET /api/status controller.LoginController.status 2 | POST /api/login controller.LoginController.login 3 | POST /api/register controller.LoginController.register 4 | GET /api/logout controller.LoginController.logout 5 | 6 | GET /api/v1/search controller.SearchController.search(location: Option[String], tag: Option[String]) 7 | GET /api/v1/searchQuery controller.SearchController.searchQuery(query:String) 8 | GET /dashboard controllers.Assets.at(path="/public", file="index-static.html") 9 | GET /register controllers.Assets.at(path="/public", file="index-static.html") 10 | GET /logout controllers.Assets.at(path="/public", file="index-static.html") 11 | GET /login controllers.Assets.at(path="/public", file="index-static.html") 12 | GET / controllers.Assets.at(path="/public", file="index-static.html") 13 | GET /assets/*file controllers.Assets.at(path="/public", file) -------------------------------------------------------------------------------- /chapter-4/web-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/favicon.ico -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/driulis-gonzalez-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/driulis-gonzalez-cover.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/driulis-gonzalez.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/driulis-gonzalez.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/flag-cu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/flag-cu.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/flag-fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/flag-fr.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/flag-jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/flag-jp.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/flag-nl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/flag-nl.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/flag-uz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/flag-uz.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/logo-judo-heroes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/logo-judo-heroes.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/mark-huizinga-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/mark-huizinga-cover.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/mark-huizinga.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/mark-huizinga.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/medal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/medal.png -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/rishod-sobirov-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/rishod-sobirov-cover.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/rishod-sobirov.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/rishod-sobirov.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/ryoko-tani-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/ryoko-tani-cover.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/ryoko-tani.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/ryoko-tani.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/teddy-riner-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/teddy-riner-cover.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/img/teddy-riner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chapter-4/web-app/public/img/teddy-riner.jpg -------------------------------------------------------------------------------- /chapter-4/web-app/public/index-static.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Seeker application 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chapter-4/web-app/test/parser/QueryParserTest.scala: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import com.microservices.search.SearchFilter 4 | import org.scalatest.{FlatSpec, Matchers} 5 | 6 | 7 | class QueryParserTest extends FlatSpec with Matchers{ 8 | 9 | 10 | "Query Parser" should "Scala in london" in { 11 | val ans = QueryParser.parse("Scala in london") 12 | ans.isLeft should be (false) 13 | ans.right.get should be (SearchFilter(Some("london"), Some("Scala"))) 14 | } 15 | 16 | "Query Parser" should "Java developers in new york" in { 17 | val ans = QueryParser.parse("Java developers in new york") 18 | println(ans) 19 | ans.isLeft should be (false) 20 | ans.right.get should be (SearchFilter(Some("new york"), Some("Java"))) 21 | } 22 | 23 | "Query Parser" should "Java DEVELOPERS iN new york" in { 24 | val ans = QueryParser.parse("Java developers in new york") 25 | println(ans) 26 | ans.isLeft should be (false) 27 | ans.right.get should be (SearchFilter(Some("new york"), Some("Java"))) 28 | } 29 | 30 | 31 | "Query Parser" should "Java developer in new york" in { 32 | val ans = QueryParser.parse("Java developer in new york") 33 | println(ans) 34 | ans.isLeft should be (false) 35 | ans.right.get should be (SearchFilter(Some("new york"), Some("Java"))) 36 | } 37 | 38 | "Query Parser" should "Java Developer in new york" in { 39 | val ans = QueryParser.parse("Java Developer in new york") 40 | println(ans) 41 | ans.isLeft should be (false) 42 | ans.right.get should be (SearchFilter(Some("new york"), Some("Java"))) 43 | } 44 | 45 | 46 | "Query Parser" should "Scala developers" in { 47 | val ans = QueryParser.parse("Scala developers") 48 | System.err.println(ans) 49 | ans.isLeft should be (false) 50 | ans.right.get should be (SearchFilter(None, Some("Scala"))) 51 | } 52 | 53 | "Query Parser" should "Scala Developers" in { 54 | val ans = QueryParser.parse("Scala Developers") 55 | System.err.println(ans) 56 | ans.isLeft should be (false) 57 | ans.right.get should be (SearchFilter(None, Some("Scala"))) 58 | } 59 | 60 | "Query Parser" should "developers in san jose" in { 61 | val ans = QueryParser.parse("developers in san jose") 62 | println(ans) 63 | 64 | ans.isLeft should be (false) 65 | ans.right.get should be (SearchFilter(Some("san jose"), None)) 66 | } 67 | // "Query Parser" should "San Francisco" in { 68 | // val ans = QueryParser.parse("San Francisco") 69 | // ans.isLeft should be (false) 70 | // ans.right.get should be (SearchFilter(None, Some("San Francisco"))) 71 | // } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /chapter-9/docker-flask/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | hello: 4 | image: hello-server:latest 5 | deploy: 6 | replicas: 3 7 | resources: 8 | limits: 9 | cpus: "0.1" 10 | memory: 50M 11 | restart_policy: 12 | condition: on-failure 13 | ports: 14 | - "5000:5000" 15 | networks: 16 | - sm_net 17 | time: 18 | image: time-server:latest 19 | deploy: 20 | replicas: 2 21 | resources: 22 | limits: 23 | cpus: "0.1" 24 | memory: 50M 25 | restart_policy: 26 | condition: on-failure 27 | ports: 28 | - "4000:4000" 29 | networks: 30 | - sm_net 31 | 32 | networks: 33 | sm_net: 34 | -------------------------------------------------------------------------------- /chapter-9/docker-flask/hello-service/Dockerfile: -------------------------------------------------------------------------------- 1 | From jfloff/alpine-python:2.7 2 | 3 | COPY server.py server.py 4 | ENV FLASK_APP server.py 5 | EXPOSE 5000 6 | RUN pip install flask 7 | RUN pip install requests 8 | CMD flask run --host=0.0.0.0 9 | 10 | -------------------------------------------------------------------------------- /chapter-9/docker-flask/hello-service/server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | import socket 3 | import requests 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | curr_time = requests.get('http://scalamicroservices_time:4000').content 9 | print curr_time 10 | hostname=socket.gethostname() 11 | return 'Hello, World! Yours truly, '+ hostname + ', Time: ' + curr_time 12 | 13 | -------------------------------------------------------------------------------- /chapter-9/docker-flask/time-service/Dockerfile: -------------------------------------------------------------------------------- 1 | From jfloff/alpine-python 2 | 3 | COPY time_server.py server.py 4 | ENV FLASK_APP server.py 5 | EXPOSE 4000 6 | RUN pip install flask 7 | CMD flask run --host=0.0.0.0 --port=4000 8 | 9 | -------------------------------------------------------------------------------- /chapter-9/docker-flask/time-service/time_server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from time import gmtime, strftime 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def get_time(): 7 | return strftime("%Y-%m-%d %H:%M:%S", gmtime()) 8 | -------------------------------------------------------------------------------- /chirper-app-complete/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | -------------------------------------------------------------------------------- /chirper-app-complete/README.md: -------------------------------------------------------------------------------- 1 | A twitter like application built using Lightbend Lagom. 2 | This is built on top of the exising [lagom-scala-chirper](https://github.com/dotta/activator-lagom-scala-chirper) by [Mirco Dotta](https://twitter.com/mircodotta). It contains a few changes when compared to original repo: 3 | * It uses the scaladsl api instead of javadsl from scala 4 | 5 | ## Run 6 | 7 | Start all services using `sbt runAll`. To access the application post the startup.: [http://localhost:9000](http://localhost:9000) 8 | * You can then signup 9 | * Add a friend 10 | * And chirp 11 | 12 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-api/src/main/scala/sample/chirper/activity/api/ActivityStreamService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.api 5 | 6 | import sample.chirper.chirp.api.Chirp 7 | 8 | import akka.stream.scaladsl.Source 9 | 10 | import akka.NotUsed 11 | import com.lightbend.lagom.scaladsl.api.ServiceCall 12 | import com.lightbend.lagom.scaladsl.api.Descriptor 13 | import com.lightbend.lagom.scaladsl.api.Service 14 | 15 | trait ActivityStreamService extends Service { 16 | 17 | def getLiveActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] 18 | 19 | def getHistoricalActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] 20 | 21 | override def descriptor(): Descriptor = { 22 | import Service._ 23 | 24 | named("activityservice").withCalls( 25 | pathCall("/api/activity/:userId/live", getLiveActivityStream _), 26 | pathCall("/api/activity/:userId/history", getHistoricalActivityStream _) 27 | ).withAutoAcl(true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-api/src/main/scala/sample/chirper/activity/api/HistoricalActivityStreamReq.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.api 5 | 6 | import java.time.Instant 7 | 8 | case class HistoricalActivityStreamReq(fromTime: Instant) 9 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.activity.impl.ActivityStreamApplicationLoader 2 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-impl/src/main/scala/sample/chirper/activity/impl/ActivityStreamModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.impl 5 | 6 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 7 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader} 8 | import com.softwaremill.macwire._ 9 | import play.api.libs.ws.ahc.AhcWSComponents 10 | import sample.chirper.activity.api.ActivityStreamService 11 | import sample.chirper.chirp.api.ChirpService 12 | import sample.chirper.friend.api.FriendService 13 | 14 | 15 | abstract class ActivityStreamModule (context: LagomApplicationContext) 16 | extends LagomApplication(context) 17 | with AhcWSComponents { 18 | lazy val friendService: FriendService = serviceClient.implement[FriendService] 19 | lazy val chirpService: ChirpService = serviceClient.implement[ChirpService] 20 | 21 | override lazy val lagomServer = serverFor[ActivityStreamService](wire[ActivityStreamServiceImpl]) 22 | } 23 | 24 | 25 | class ActivityStreamApplicationLoader extends LagomApplicationLoader { 26 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 27 | new ActivityStreamModule(context) with LagomDevModeComponents 28 | 29 | override def load(context: LagomApplicationContext): LagomApplication = 30 | // new FriendModule(context) with ConductRApplicationComponents 31 | new ActivityStreamModule(context) with LagomDevModeComponents 32 | 33 | override def describeService = Some(readDescriptor[ActivityStreamService]) 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /chirper-app-complete/activity-stream-impl/src/main/scala/sample/chirper/activity/impl/ActivityStreamServiceImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.impl 5 | 6 | import java.time.Duration 7 | import java.time.Instant 8 | 9 | import scala.compat.java8.FutureConverters._ 10 | import scala.concurrent.ExecutionContext 11 | 12 | import com.lightbend.lagom.scaladsl.api.ServiceCall 13 | 14 | import akka.NotUsed 15 | import akka.stream.scaladsl.Source 16 | 17 | import sample.chirper.activity.api.ActivityStreamService 18 | import sample.chirper.chirp.api.Chirp 19 | import sample.chirper.chirp.api.ChirpService 20 | import sample.chirper.chirp.api.HistoricalChirpsRequest 21 | import sample.chirper.chirp.api.LiveChirpsRequest 22 | import sample.chirper.friend.api.FriendService 23 | 24 | class ActivityStreamServiceImpl ( 25 | friendService: FriendService, 26 | chirpService: ChirpService)(implicit ec: ExecutionContext) extends ActivityStreamService { 27 | 28 | 29 | override def getLiveActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] = { 30 | req => 31 | for { 32 | user <- friendService.getUser(userId).invoke() 33 | userIds = user.friends :+ userId 34 | chirpsReq = LiveChirpsRequest(userIds) 35 | chirps <- chirpService.getLiveChirps.invoke(chirpsReq) 36 | } yield chirps 37 | } 38 | 39 | override def getHistoricalActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] = { 40 | req => 41 | for { 42 | user <- friendService.getUser(userId).invoke() 43 | userIds = user.friends :+ userId 44 | // FIXME we should use HistoricalActivityStreamReq request parameter 45 | fromTime = Instant.now().minus(Duration.ofDays(7)) 46 | chirpsReq = HistoricalChirpsRequest(fromTime, userIds) 47 | chirps <- chirpService.getHistoricalChirps.invoke(chirpsReq) 48 | } yield chirps 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /chirper-app-complete/build.sbt: -------------------------------------------------------------------------------- 1 | name := "chirper-app-complete" 2 | 3 | organization in ThisBuild := "sample.chirper" 4 | 5 | scalaVersion in ThisBuild := "2.11.8" 6 | 7 | val macwire = "com.softwaremill.macwire" %% "macros" % "2.2.5" % "provided" 8 | 9 | lazy val friendApi = project("friend-api") 10 | .settings( 11 | version := "1.0-SNAPSHOT", 12 | libraryDependencies += lagomScaladslApi 13 | ) 14 | 15 | lazy val friendImpl = project("friend-impl") 16 | .enablePlugins(LagomScala) 17 | .settings( 18 | version := "1.0-SNAPSHOT", 19 | libraryDependencies ++= Seq( 20 | lagomScaladslTestKit, 21 | lagomScaladslPersistenceCassandra, 22 | "com.datastax.cassandra" % "cassandra-driver-extras" % "3.0.0", 23 | lagomScaladslKafkaBroker, 24 | macwire 25 | ) 26 | ) 27 | .settings(lagomForkedTestSettings: _*) 28 | .dependsOn(friendApi) 29 | 30 | lazy val chirpApi = project("chirp-api") 31 | .settings( 32 | version := "1.0-SNAPSHOT", 33 | libraryDependencies ++= Seq( 34 | lagomScaladslApi 35 | ) 36 | ) 37 | 38 | lazy val chirpImpl = project("chirp-impl") 39 | .enablePlugins(LagomScala) 40 | .settings( 41 | version := "1.0-SNAPSHOT", 42 | libraryDependencies ++= Seq( 43 | lagomScaladslPubSub, 44 | lagomScaladslTestKit, 45 | lagomScaladslPersistenceCassandra, 46 | macwire 47 | ) 48 | ) 49 | .settings(lagomForkedTestSettings: _*) 50 | .dependsOn(chirpApi) 51 | 52 | lazy val activityStreamApi = project("activity-stream-api") 53 | .settings( 54 | version := "1.0-SNAPSHOT", 55 | libraryDependencies += lagomScaladslApi 56 | ) 57 | .dependsOn(chirpApi) 58 | 59 | lazy val activityStreamImpl = project("activity-stream-impl") 60 | .enablePlugins(LagomScala) 61 | .settings( 62 | version := "1.0-SNAPSHOT", 63 | libraryDependencies ++= Seq( 64 | lagomScaladslTestKit, 65 | macwire 66 | ) 67 | ) 68 | .dependsOn(activityStreamApi, chirpApi, friendApi) 69 | 70 | lazy val friendRecommendationApi = project("friend-recommendation-api") 71 | .settings( 72 | version := "1.0-SNAPSHOT", 73 | libraryDependencies += lagomScaladslApi 74 | ) 75 | 76 | lazy val friendRecommendationImpl = project("friend-recommendation-impl") 77 | .enablePlugins(LagomScala) 78 | .settings( 79 | version := "1.0-SNAPSHOT", 80 | libraryDependencies ++= Seq( 81 | lagomScaladslTestKit, 82 | lagomScaladslKafkaClient, 83 | macwire 84 | ) 85 | ) 86 | .dependsOn(friendRecommendationApi, friendApi) 87 | 88 | 89 | lazy val frontEnd = project("front-end") 90 | .enablePlugins(PlayScala, LagomPlay) 91 | .settings( 92 | version := "1.0-SNAPSHOT", 93 | routesGenerator := StaticRoutesGenerator, 94 | libraryDependencies ++= Seq( 95 | "org.webjars" % "react" % "0.14.3", 96 | "org.webjars" % "react-router" % "1.0.3", 97 | "org.webjars" % "jquery" % "2.2.0", 98 | "org.webjars" % "foundation" % "5.3.0", 99 | macwire, 100 | lagomScaladslServer 101 | ), 102 | ReactJsKeys.sourceMapInline := true 103 | ) 104 | 105 | def project(id: String) = Project(id, base = file(id)) 106 | .settings( 107 | scalacOptions in Compile += "-Xexperimental" // this enables Scala lambdas to be passed as Scala SAMs 108 | ) 109 | .settings( 110 | libraryDependencies ++= Seq( 111 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.7.3" // actually, only api projects need this 112 | ) 113 | ) 114 | 115 | licenses in ThisBuild := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) 116 | 117 | lagomCassandraEnabled in ThisBuild := true 118 | 119 | // do not delete database files on start 120 | lagomCassandraCleanOnStart in ThisBuild := false 121 | 122 | lagomCassandraPort in ThisBuild := 4042 123 | 124 | lagomKafkaEnabled in ThisBuild := true -------------------------------------------------------------------------------- /chirper-app-complete/chirp-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-complete/chirp-api/src/main/scala/sample/chirper/chirp/api/Chirp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import java.time.Instant 7 | import java.util.UUID 8 | 9 | import play.api.libs.functional.syntax._ 10 | import play.api.libs.json.{JsPath, Json, Reads} // Combinator syntax 11 | 12 | case class Chirp (userId: String, message: String,timestamp: Instant, uuid: String) { 13 | def this(userId: String, message: String) = 14 | this(userId, message, Chirp.defaultTimestamp, Chirp.defaultUUID) 15 | } 16 | 17 | object Chirp { 18 | implicit object ChirpOrdering extends Ordering[Chirp] { 19 | override def compare(x: Chirp, y: Chirp): Int = x.timestamp.compareTo(y.timestamp) 20 | } 21 | 22 | def apply(userId: String, message: String, timestamp: Option[Instant], uuid: Option[String]): Chirp = 23 | new Chirp(userId, message, timestamp.getOrElse(defaultTimestamp), uuid.getOrElse(defaultUUID)) 24 | 25 | private def defaultTimestamp = Instant.now() 26 | private def defaultUUID = UUID.randomUUID().toString 27 | 28 | implicit val chirpRead: Reads[Chirp] = ( 29 | (JsPath \ "userId").read[String] and 30 | (JsPath \ "message").read[String] 31 | )((userId, message) => Chirp(userId, message, None, None)) 32 | 33 | implicit val chirpWrite = Json.writes[Chirp] 34 | } -------------------------------------------------------------------------------- /chirper-app-complete/chirp-api/src/main/scala/sample/chirper/chirp/api/ChirpService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import akka.stream.scaladsl.Source 7 | import akka.{Done, NotUsed} 8 | import com.lightbend.lagom.scaladsl.api.Descriptor 9 | import com.lightbend.lagom.scaladsl.api.Service 10 | import com.lightbend.lagom.scaladsl.api.ServiceCall 11 | 12 | trait ChirpService extends Service { 13 | 14 | def addChirp(userId: String): ServiceCall[Chirp, Done] 15 | 16 | def getLiveChirps: ServiceCall[LiveChirpsRequest, Source[Chirp, NotUsed]] 17 | 18 | def getHistoricalChirps: ServiceCall[HistoricalChirpsRequest, Source[Chirp, NotUsed]] 19 | 20 | override def descriptor(): Descriptor = { 21 | import Service._ 22 | 23 | named("chirpservice").withCalls( 24 | pathCall("/api/chirps/live/:userId", addChirp _), 25 | namedCall("/api/chirps/history", getHistoricalChirps _), 26 | namedCall("/api/chirps/live", getLiveChirps _) 27 | ).withAutoAcl(true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chirper-app-complete/chirp-api/src/main/scala/sample/chirper/chirp/api/HistoricalChirpsRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import java.time.Instant 7 | 8 | import play.api.libs.json.Json 9 | 10 | import scala.collection.immutable.Seq 11 | 12 | case class HistoricalChirpsRequest(fromTime: Instant, userIds: Seq[String]) 13 | 14 | object HistoricalChirpsRequest{ 15 | implicit val format = Json.format[HistoricalChirpsRequest] 16 | } 17 | -------------------------------------------------------------------------------- /chirper-app-complete/chirp-api/src/main/scala/sample/chirper/chirp/api/LiveChirpsRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import play.api.libs.json.Json 7 | 8 | import scala.collection.immutable.Seq 9 | 10 | case class LiveChirpsRequest(userIds: Seq[String]) 11 | 12 | object LiveChirpsRequest{ 13 | implicit val liveChirpsRequestFormat = Json.format[LiveChirpsRequest] 14 | } 15 | -------------------------------------------------------------------------------- /chirper-app-complete/chirp-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.chirp.impl.ChirpApplicationLoader 2 | 3 | chirp-service.cassandra.keyspace = chrip_service 4 | 5 | cassandra-journal.keyspace = ${chirp-service.cassandra.keyspace} 6 | cassandra-snapshot-store.keyspace = ${chirp-service.cassandra.keyspace} 7 | lagom.persistence.read-side.cassandra.keyspace = ${chirp-service.cassandra.keyspace} 8 | -------------------------------------------------------------------------------- /chirper-app-complete/chirp-impl/src/main/scala/sample/chirper/chirp/impl/ChirpModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.impl 5 | 6 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 7 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraPersistenceComponents 8 | import com.lightbend.lagom.scaladsl.playjson.{JsonSerializer, JsonSerializerRegistry} 9 | import com.lightbend.lagom.scaladsl.pubsub.PubSubComponents 10 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader} 11 | import com.softwaremill.macwire._ 12 | import play.api.libs.ws.ahc.AhcWSComponents 13 | import sample.chirper.chirp.api.ChirpService 14 | 15 | import scala.collection.immutable 16 | 17 | abstract class ChirpModule(context: LagomApplicationContext) 18 | extends LagomApplication(context) 19 | with AhcWSComponents 20 | with PubSubComponents 21 | with CassandraPersistenceComponents 22 | { 23 | override lazy val lagomServer = serverFor[ChirpService](wire[ChirpServiceImpl]) 24 | override def jsonSerializerRegistry = new JsonSerializerRegistry { 25 | override def serializers = List() 26 | } 27 | } 28 | 29 | class ChirpApplicationLoader extends LagomApplicationLoader { 30 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 31 | new ChirpModule(context) with LagomDevModeComponents 32 | 33 | override def load(context: LagomApplicationContext): LagomApplication = 34 | // new ChirpModule(context) with ConductRApplicationComponents 35 | new ChirpModule(context) with LagomDevModeComponents 36 | 37 | override def describeService = Some(readDescriptor[ChirpService]) 38 | } -------------------------------------------------------------------------------- /chirper-app-complete/chirp-impl/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-api/src/main/scala/sample/chirper/friend/api/FriendId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import play.api.libs.json.Json 7 | 8 | case class FriendId(friendId: String) 9 | 10 | object FriendId { 11 | implicit val friendIdJson = Json.format[FriendId] 12 | } 13 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-api/src/main/scala/sample/chirper/friend/api/FriendService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import akka.{Done, NotUsed} 7 | import com.lightbend.lagom.scaladsl.api.Descriptor 8 | import com.lightbend.lagom.scaladsl.api.Service 9 | import com.lightbend.lagom.scaladsl.api.ServiceCall 10 | import com.lightbend.lagom.scaladsl.api.broker.Topic 11 | import com.lightbend.lagom.scaladsl.api.broker.kafka.{KafkaProperties, PartitionKeyStrategy} 12 | 13 | import scala.collection.immutable.Seq 14 | 15 | object FriendService { 16 | val TOPIC_NAME = "friends" 17 | } 18 | 19 | /** 20 | * The friend service. 21 | */ 22 | trait FriendService extends Service { 23 | 24 | /** 25 | * Service call for getting a user. 26 | * 27 | * The ID of this service call is the user name, and the response message is the User object. 28 | */ 29 | def getUser(id: String): ServiceCall[NotUsed, User] 30 | 31 | /** 32 | * Service call for creating a user. 33 | * 34 | * The request message is the User to create. 35 | */ 36 | def createUser(): ServiceCall[CreateUser, Done] 37 | 38 | /** 39 | * Service call for adding a friend to a user. 40 | * 41 | * The ID for this service call is the ID of the user that the friend is being added to. 42 | * The request message is the ID of the friend being added. 43 | */ 44 | def addFriend(userId: String): ServiceCall[FriendId, Done] 45 | 46 | /** 47 | * Service call for getting the followers of a user. 48 | * 49 | * The ID for this service call is the Id of the user to get the followers for. 50 | * The response message is the list of follower IDs. 51 | */ 52 | def getFollowers(userId: String): ServiceCall[NotUsed, Seq[String]] 53 | 54 | //added wrt message api 55 | def friendsTopic: Topic[KFriendMessage] 56 | 57 | override def descriptor(): Descriptor = { 58 | import Service._ 59 | 60 | named("friendservice").withCalls( 61 | pathCall("/api/users/:id", getUser _), 62 | namedCall("/api/users", createUser), 63 | pathCall("/api/users/:userId/friends", addFriend _), 64 | pathCall("/api/users/:id/followers", getFollowers _) 65 | ).withTopics( 66 | topic(FriendService.TOPIC_NAME, friendsTopic) 67 | .addProperty( 68 | KafkaProperties.partitionKeyStrategy, 69 | PartitionKeyStrategy[KFriendMessage](_ => "0") 70 | ) 71 | ).withAutoAcl(true) 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-api/src/main/scala/sample/chirper/friend/api/User.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import java.time.Instant 7 | 8 | import play.api.libs.json.{JsValue, Json, Reads, Writes} 9 | 10 | import scala.collection.immutable.Seq 11 | 12 | case class CreateUser(userId: String, name: String) 13 | 14 | case class User(userId: String, name: String, friends: Seq[String]) { 15 | def this(userId: String, name: String) = this(userId, name, Seq.empty) 16 | } 17 | 18 | sealed trait KFriendMessage 19 | 20 | case class KUserCreated(userId: String, name: String, timestamp: Instant = Instant.now()) extends KFriendMessage 21 | 22 | case class KFriendAdded(userId: String, friendId: String, timestamp: Instant = Instant.now()) extends KFriendMessage 23 | 24 | 25 | object CreateUser { 26 | implicit val createUserJson = Json.format[CreateUser] 27 | } 28 | 29 | object User { 30 | implicit val userJson = Json.format[User] 31 | 32 | def apply(createUser: CreateUser): User = User(createUser.userId, createUser.name, Seq()) 33 | // def apply (userId: String, name: String, friends: Seq[String]) = new User(userId, name, friends) 34 | } 35 | 36 | object KFriendMessage{ 37 | implicit val writes = new Writes[KFriendMessage]{ 38 | override def writes(o: KFriendMessage) = o match { 39 | case x: KUserCreated => KUserCreated.format.writes(x) 40 | case x: KFriendAdded => KFriendAdded.format.writes(x) 41 | } 42 | } 43 | 44 | implicit val reads = new Reads[KFriendMessage]{ 45 | override def reads(json: JsValue) = 46 | if(json.validate(KUserCreated.format).isSuccess) 47 | json.validate(KUserCreated.format) 48 | else if(json.validate(KFriendAdded.format).isSuccess) 49 | json.validate(KFriendAdded.format) 50 | else throw new RuntimeException("no valid format found") 51 | } 52 | } 53 | 54 | object KUserCreated { 55 | implicit val format = Json.format[KUserCreated] 56 | } 57 | 58 | object KFriendAdded { 59 | implicit val format = Json.format[KFriendAdded] 60 | } 61 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.friend.impl.FriendApplicationLoader 2 | 3 | friend-service.cassandra.keyspace = friend_service 4 | 5 | cassandra-journal.keyspace = ${friend-service.cassandra.keyspace} 6 | cassandra-snapshot-store.keyspace = ${friend-service.cassandra.keyspace} 7 | lagom.persistence.read-side.cassandra.keyspace = ${friend-service.cassandra.keyspace} 8 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendCommand.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity 7 | import akka.Done 8 | import play.api.libs.json.{JsObject, Json, OWrites, Reads} 9 | import sample.chirper.friend.api.User 10 | 11 | 12 | sealed trait FriendCommand 13 | 14 | case class CreateUserCommand(user: User) extends PersistentEntity.ReplyType[Done] with FriendCommand 15 | case class GetUser() extends PersistentEntity.ReplyType[GetUserReply] with FriendCommand 16 | 17 | case class GetUserReply(user: Option[User]) 18 | case class AddFriend(friendUserId: String) extends PersistentEntity.ReplyType[Done] with FriendCommand 19 | 20 | object CreateUserCommand{ 21 | implicit val format = Json.format[CreateUserCommand] 22 | } 23 | object GetUser{ 24 | implicit val strictReads = Reads[GetUser](json => json.validate[JsObject].filter(_.values.isEmpty).map(_ => GetUser())) 25 | implicit val writes = OWrites[GetUser](_ => Json.obj()) 26 | } 27 | object GetUserReply{ 28 | implicit val format = Json.format[GetUserReply] 29 | } 30 | object AddFriend{ 31 | implicit val format = Json.format[AddFriend] 32 | } -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendEntity.scala: -------------------------------------------------------------------------------- 1 | package sample.chirper.friend.impl 2 | 3 | import akka.Done 4 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity 5 | import sample.chirper.friend.api.User 6 | 7 | class FriendEntity extends PersistentEntity { 8 | override type Command = FriendCommand 9 | override type Event = FriendEvent 10 | override type State = FriendState 11 | 12 | override def initialState: FriendState = FriendState(None) 13 | 14 | override def behavior: ((FriendState) => Actions) = { 15 | case FriendState(None) => 16 | userNotCreated 17 | 18 | case FriendState(Some(x)) => 19 | Actions() 20 | .onCommand[CreateUserCommand, Done] { 21 | case (CreateUserCommand(user), ctx, state) => 22 | ctx.invalidCommand(s"User $x is already created") 23 | ctx.done 24 | } 25 | .orElse(addFriend) 26 | .orElse(getUserCommand) 27 | } 28 | 29 | private val getUserCommand = Actions().onReadOnlyCommand[GetUser, GetUserReply] { 30 | case (GetUser(), ctx, state) => ctx.reply(GetUserReply(state.user)) 31 | } 32 | 33 | 34 | val userNotCreated = { 35 | Actions() 36 | .onCommand[CreateUserCommand, Done] { 37 | case (CreateUserCommand(user), ctx, state) => 38 | val event = UserCreated(user.userId, user.name) 39 | ctx.thenPersist(event) { _ => 40 | ctx.reply(Done) 41 | } 42 | }.onEvent { 43 | case (UserCreated(userId, name, ts), state) => FriendState(User(userId, name, List())) 44 | } 45 | .onReadOnlyCommand[GetUser, GetUserReply] { 46 | case (GetUser(), ctx, state) => 47 | ctx.reply(GetUserReply(None)) 48 | } 49 | } 50 | 51 | val addFriend = { 52 | Actions().onCommand[AddFriend, Done] { 53 | case (AddFriend(id), ctx, FriendState(Some(user))) if user.friends.contains(id) => 54 | //user already had the requested command as a friend 55 | ctx.reply(Done) 56 | ctx.done 57 | case (AddFriend(friendUserId), ctx, FriendState(Some(user))) => 58 | ctx.thenPersist(FriendAdded(user.userId, friendUserId))(evt => ctx.reply(Done)) 59 | }.onEvent { 60 | case (FriendAdded(userId, friendId, ts), state) => state.addFriend(friendId) 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendEventProcessor.scala: -------------------------------------------------------------------------------- 1 | package sample.chirper.friend.impl 2 | 3 | 4 | import java.util.UUID 5 | 6 | import akka.Done 7 | 8 | import com.datastax.driver.core.PreparedStatement 9 | import com.lightbend.lagom.scaladsl.persistence.{AggregateEventTag, EventStreamElement, ReadSideProcessor} 10 | import com.lightbend.lagom.scaladsl.persistence.cassandra.{CassandraReadSide, CassandraSession} 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | class FriendEventProcessor(session: CassandraSession, readSide: CassandraReadSide)(implicit ec: ExecutionContext) 14 | extends ReadSideProcessor[FriendEvent] { 15 | 16 | @volatile private var writeFollowers: PreparedStatement = null // initialized in prepare 17 | @volatile private var writeOffset: PreparedStatement = null // initialized in prepare 18 | 19 | 20 | def prepare(session: CassandraSession) = { 21 | for{ 22 | _ <- prepareCreateTables(session) 23 | _ <- prepareWriteFollowers(session) 24 | _ <- prepareWriteOffset(session) 25 | } yield selectOffset(session) 26 | } 27 | 28 | private def prepareCreateTables(session: CassandraSession) = { 29 | for { 30 | _ <- session.executeCreateTable( 31 | """CREATE TABLE IF NOT EXISTS follower ( 32 | userId text, followedBy text, 33 | PRIMARY KEY (userId, followedBy))""") 34 | _ <- session.executeCreateTable( 35 | """CREATE TABLE IF NOT EXISTS friend_offset ( 36 | partition int, offset timeuuid, 37 | PRIMARY KEY (partition))""") 38 | 39 | } yield Done 40 | } 41 | 42 | private def prepareWriteFollowers(session: CassandraSession) = { 43 | val statement = session.prepare("INSERT INTO follower (userId, followedBy) VALUES (?, ?)") 44 | statement.map(ps => { 45 | this.writeFollowers = ps 46 | Done 47 | }) 48 | } 49 | 50 | private def prepareWriteOffset(session: CassandraSession) = { 51 | val statement = session.prepare("INSERT INTO friend_offset (partition, offset) VALUES (1, ?)") 52 | statement.map(ps => { 53 | this.writeOffset = ps 54 | Done 55 | }) 56 | } 57 | 58 | private def selectOffset(session: CassandraSession) = { 59 | val select = session.selectOne("SELECT offset FROM friend_offset WHERE partition=1") 60 | select.map { maybeRow => maybeRow.map[UUID](_.getUUID("offset")) } 61 | } 62 | 63 | // override def defineEventHandlers(builder: EventHandlersBuilder): EventHandlers = { 64 | // builder.setEventHandler(classOf[FriendAdded], processFriendChanged) 65 | // builder.build() 66 | // } 67 | 68 | private def processFriendChanged(event: FriendAdded, offset: UUID) = { 69 | val bindWriteFollowers = writeFollowers.bind() 70 | bindWriteFollowers.setString("userId", event.friendId) 71 | bindWriteFollowers.setString("followedBy", event.userId) 72 | val bindWriteOffset = writeOffset.bind(offset) 73 | Future.successful(List(bindWriteFollowers, bindWriteOffset)) 74 | } 75 | 76 | 77 | override def buildHandler() = { 78 | readSide.builder[FriendEvent]("friendEventOffset") 79 | .setGlobalPrepare(() => prepareCreateTables(session)) 80 | .setPrepare(_ => prepareWriteFollowers(session)) 81 | .setEventHandler[FriendAdded]((e: EventStreamElement[FriendAdded]) => processFriendChanged(e.event, UUID.fromString(e.entityId))) 82 | .build() 83 | } 84 | 85 | override def aggregateTags = Set(FriendEvent.Tag) 86 | } -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendEvents.scala: -------------------------------------------------------------------------------- 1 | package sample.chirper.friend.impl 2 | 3 | import com.lightbend.lagom.scaladsl.persistence.AggregateEvent 4 | import com.lightbend.lagom.scaladsl.persistence.AggregateEventTag 5 | import java.time.Instant 6 | 7 | import play.api.libs.json.Json 8 | 9 | object FriendEvent { 10 | val Tag = AggregateEventTag[FriendEvent] 11 | } 12 | sealed trait FriendEvent extends AggregateEvent[FriendEvent] { 13 | override def aggregateTag(): AggregateEventTag[FriendEvent] = FriendEvent.Tag 14 | } 15 | 16 | case class UserCreated(userId: String, name: String, timestamp: Instant = Instant.now()) extends FriendEvent 17 | 18 | case class FriendAdded(userId: String, friendId: String, timestamp: Instant = Instant.now()) extends FriendEvent 19 | 20 | object UserCreated{ 21 | implicit val format = Json.format[UserCreated] 22 | } 23 | object FriendAdded{ 24 | implicit val format = Json.format[FriendAdded] 25 | } 26 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | 7 | import com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaComponents 8 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 9 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraPersistenceComponents 10 | import com.lightbend.lagom.scaladsl.playjson.{JsonSerializer, JsonSerializerRegistry} 11 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader, LagomServer} 12 | import com.softwaremill.macwire.wire 13 | import play.api.libs.ws.ahc.AhcWSComponents 14 | import sample.chirper.friend.api.FriendService 15 | 16 | import scala.collection.immutable 17 | 18 | abstract class FriendModule (context: LagomApplicationContext) 19 | extends LagomApplication(context) 20 | with AhcWSComponents 21 | with CassandraPersistenceComponents 22 | with LagomKafkaComponents 23 | { 24 | 25 | persistentEntityRegistry.register(wire[FriendEntity]) 26 | // readSide.register(wire[FriendEventProcessor]) 27 | override def jsonSerializerRegistry = FriendSerializerRegistry 28 | 29 | override lazy val lagomServer: LagomServer = serverFor[FriendService](wire[FriendServiceImpl]) 30 | } 31 | 32 | class FriendApplicationLoader extends LagomApplicationLoader { 33 | 34 | override def load(context: LagomApplicationContext): LagomApplication = 35 | new FriendModule(context) with LagomDevModeComponents 36 | 37 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 38 | new FriendModule(context) with LagomDevModeComponents 39 | 40 | override def describeService = Some(readDescriptor[FriendService]) 41 | } 42 | 43 | /** 44 | * This is telling Lagom to use the below serializers for persistence. One could use some other format 45 | * apart from Json, and if you do, you need to declare it here. 46 | * Lagom by default provides support for json. 47 | */ 48 | object FriendSerializerRegistry extends JsonSerializerRegistry { 49 | override def serializers = List( 50 | JsonSerializer[CreateUserCommand], 51 | JsonSerializer[GetUser], 52 | JsonSerializer[GetUserReply], 53 | JsonSerializer[AddFriend], 54 | JsonSerializer[UserCreated], 55 | JsonSerializer[FriendAdded], 56 | JsonSerializer[FriendState] 57 | ) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendServiceImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | import akka.{Done, NotUsed} 7 | import com.lightbend.lagom.scaladsl.api.ServiceCall 8 | import com.lightbend.lagom.scaladsl.api.transport.NotFound 9 | import com.lightbend.lagom.scaladsl.broker.TopicProducer 10 | import com.lightbend.lagom.scaladsl.persistence.{EventStreamElement, PersistentEntityRegistry} 11 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraReadSide 12 | import sample.chirper.friend.api._ 13 | import com.lightbend.lagom.scaladsl.persistence.cassandra.CassandraSession 14 | import com.lightbend.lagom.scaladsl.persistence.{PersistentEntityRef, PersistentEntityRegistry} 15 | import sample.chirper.friend.api.{CreateUser, FriendId, FriendService, User} 16 | 17 | import scala.collection.immutable.Seq 18 | import scala.concurrent.ExecutionContext 19 | 20 | class FriendServiceImpl(persistentEntities: PersistentEntityRegistry, 21 | db: CassandraSession)(implicit ec: ExecutionContext) extends FriendService { 22 | 23 | override def getUser(id: String): ServiceCall[NotUsed, User] = { 24 | request => 25 | friendEntityRef(id).ask(GetUser()) 26 | .map(_.user.getOrElse(throw NotFound(s"user $id not found"))) 27 | } 28 | 29 | override def createUser(): ServiceCall[CreateUser, Done] = { 30 | request => 31 | friendEntityRef(request.userId).ask(CreateUserCommand(User(request))) 32 | } 33 | 34 | override def addFriend(userId: String): ServiceCall[FriendId, Done] = { 35 | request => 36 | friendEntityRef(userId).ask(AddFriend(request.friendId)) 37 | } 38 | 39 | override def getFollowers(id: String): ServiceCall[NotUsed, Seq[String]] = { 40 | req => { 41 | db.selectAll("SELECT * FROM follower WHERE userId = ?", id).map { jrows => 42 | val rows = jrows.toVector 43 | rows.map(_.getString("followedBy")) 44 | } 45 | } 46 | } 47 | 48 | 49 | private def friendEntityRef(userId: String): PersistentEntityRef[FriendCommand] = 50 | persistentEntities.refFor[FriendEntity](userId) 51 | 52 | override def friendsTopic = TopicProducer.singleStreamWithOffset { 53 | fromOffset => 54 | println("friendsTopic caled "+fromOffset) 55 | persistentEntities.eventStream(FriendEvent.Tag, fromOffset) 56 | .map(ev => { 57 | println("sender: "+ev) 58 | (convertEvent(ev), ev.offset) 59 | }) 60 | } 61 | 62 | def convertEvent(helloEvent: EventStreamElement[FriendEvent]): KFriendMessage = { 63 | helloEvent.event match { 64 | case UserCreated(userId, name, ts) => KUserCreated(userId, name, ts) 65 | case FriendAdded(userId, friendId, ts) => KFriendAdded(userId, friendId, ts) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendState.scala: -------------------------------------------------------------------------------- 1 | package sample.chirper.friend.impl 2 | 3 | import play.api.libs.json.Json 4 | import sample.chirper.friend.api.User 5 | 6 | 7 | case class FriendState(user: Option[User]) { 8 | def addFriend(friendUserId: String): FriendState = user match { 9 | case None => throw new IllegalStateException("friend can't be added before user is created") 10 | case Some(x) => 11 | val newFriends = x.friends :+ friendUserId 12 | FriendState(Some(x.copy(friends = newFriends))) 13 | } 14 | } 15 | 16 | object FriendState { 17 | def apply(user: User): FriendState = FriendState(Option(user)) 18 | 19 | def emptyState: FriendState = FriendState(None) 20 | 21 | implicit val format = Json.format[FriendState] 22 | } 23 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/test/scala/sample/chirper/friend/impl/FriendEntityTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | import java.util.Collections 7 | 8 | import scala.collection.immutable.Seq 9 | 10 | import org.junit.AfterClass 11 | import org.junit.Assert.assertEquals 12 | import org.junit.BeforeClass 13 | import org.junit.Test 14 | 15 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity 16 | import com.lightbend.lagom.scaladsl.testkit.PersistentEntityTestDriver 17 | 18 | import akka.Done 19 | import akka.actor.ActorSystem 20 | import akka.testkit.JavaTestKit 21 | import sample.chirper.friend.api.User 22 | 23 | 24 | object FriendEntityTest { 25 | @volatile private var system: ActorSystem = _ 26 | @BeforeClass 27 | def setup(): Unit = { 28 | system = ActorSystem.create("FriendEntityTest") 29 | } 30 | 31 | @AfterClass 32 | def teardown(): Unit = { 33 | JavaTestKit.shutdownActorSystem(system) 34 | system = null 35 | } 36 | } 37 | 38 | class FriendEntityTest { 39 | 40 | import FriendEntityTest.system 41 | @Test 42 | def testCreateUser(): Unit = { 43 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 44 | 45 | val outcome = driver.run(CreateUser(new User("alice", "Alice"))) 46 | assertEquals(Done, outcome.getReplies.get(0)) 47 | assertEquals("alice", outcome.events.get(0).asInstanceOf[UserCreated].userId) 48 | assertEquals("Alice", outcome.events.get(0).asInstanceOf[UserCreated].name) 49 | assertEquals(Collections.emptyList(), driver.getAllIssues) 50 | } 51 | 52 | @Test 53 | def testRejectDuplicateCreate(): Unit = { 54 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 55 | driver.run(new CreateUser(new User("alice", "Alice"))); 56 | 57 | val outcome = driver.run(CreateUser(new User("alice", "Alice"))) 58 | assertEquals(classOf[PersistentEntity.InvalidCommandException], outcome.getReplies.get(0).getClass()) 59 | assertEquals(Collections.emptyList(), outcome.events) 60 | assertEquals(Collections.emptyList(), driver.getAllIssues) 61 | } 62 | 63 | @Test 64 | def testCreateUserWithInitialFriends(): Unit = { 65 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 66 | 67 | val friends = Seq("bob", "peter") 68 | val outcome = driver.run(CreateUser(User("alice", "Alice", friends))) 69 | assertEquals(Done, outcome.getReplies.get(0)) 70 | assertEquals("alice", outcome.events.get(0).asInstanceOf[UserCreated].userId) 71 | assertEquals("bob", outcome.events.get(1).asInstanceOf[FriendAdded].friendId) 72 | assertEquals("peter", outcome.events.get(2).asInstanceOf[FriendAdded].friendId) 73 | assertEquals(Collections.emptyList(), driver.getAllIssues) 74 | } 75 | 76 | @Test 77 | def testAddFriend(): Unit = { 78 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 79 | driver.run(CreateUser(new User("alice", "Alice"))) 80 | 81 | val outcome = driver.run(AddFriend("bob"), AddFriend("peter")) 82 | assertEquals(Done, outcome.getReplies.get(0)) 83 | assertEquals("bob", outcome.events.get(0).asInstanceOf[FriendAdded].friendId) 84 | assertEquals("peter", outcome.events.get(1).asInstanceOf[FriendAdded].friendId) 85 | assertEquals(Collections.emptyList(), driver.getAllIssues) 86 | } 87 | 88 | @Test 89 | def testAddDuplicateFriend(): Unit = { 90 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 91 | driver.run(CreateUser(new User("alice", "Alice"))) 92 | driver.run(AddFriend("bob"), AddFriend("peter")) 93 | 94 | val outcome = driver.run(AddFriend("bob")) 95 | assertEquals(Done, outcome.getReplies.get(0)) 96 | assertEquals(Collections.emptyList(), outcome.events) 97 | assertEquals(Collections.emptyList(), driver.getAllIssues) 98 | } 99 | 100 | @Test 101 | def testGetUser(): Unit = { 102 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 103 | val alice = new User("alice", "Alice") 104 | driver.run(CreateUser(alice)) 105 | 106 | val outcome = driver.run(GetUser()) 107 | assertEquals(GetUserReply(Some(alice)), outcome.getReplies.get(0)) 108 | assertEquals(Collections.emptyList(), outcome.events) 109 | assertEquals(Collections.emptyList(), driver.getAllIssues) 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-impl/src/test/scala/sample/chirper/friend/impl/FriendServiceTest.scala: -------------------------------------------------------------------------------- 1 | ///* 2 | // * Copyright (C) 2016 Lightbend Inc. 3 | // */ 4 | //package sample.chirper.friend.impl 5 | // 6 | //import java.util.concurrent.TimeUnit.SECONDS 7 | // 8 | //import scala.collection.immutable.Seq 9 | //import scala.concurrent.duration.FiniteDuration 10 | // 11 | //import org.junit.Assert.assertEquals 12 | //import org.junit.Test 13 | // 14 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.defaultSetup 15 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.eventually 16 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.withServer 17 | // 18 | //import akka.NotUsed 19 | //import sample.chirper.friend.api.FriendId 20 | //import sample.chirper.friend.api.FriendService 21 | //import sample.chirper.friend.api.User 22 | // 23 | //class FriendServiceTest { 24 | // 25 | // 26 | // @throws(classOf[Exception]) 27 | // @Test 28 | // def shouldBeAbleToCreateUsersAndConnectFriends() { 29 | // withServer(defaultSetup, server => { 30 | // val friendService = server.client(classOf[FriendService]) 31 | // val usr1 = new User("usr1", "User 1"); 32 | // friendService.createUser().invoke(usr1).toCompletableFuture().get(10, SECONDS) 33 | // val usr2 = new User("usr2", "User 2"); 34 | // friendService.createUser().invoke(usr2).toCompletableFuture().get(3, SECONDS) 35 | // val usr3 = new User("usr3", "User 3"); 36 | // friendService.createUser().invoke(usr3).toCompletableFuture().get(3, SECONDS) 37 | // 38 | // friendService.addFriend("usr1").invoke(FriendId(usr2.userId)).toCompletableFuture().get(3, SECONDS) 39 | // friendService.addFriend("usr1").invoke(FriendId(usr3.userId)).toCompletableFuture().get(3, SECONDS) 40 | // 41 | // val fetchedUsr1 = friendService.getUser("usr1").invoke(NotUsed).toCompletableFuture().get(3, 42 | // SECONDS) 43 | // assertEquals(usr1.userId, fetchedUsr1.userId) 44 | // assertEquals(usr1.name, fetchedUsr1.name) 45 | // assertEquals(Seq("usr2", "usr3"), fetchedUsr1.friends) 46 | // 47 | // eventually(FiniteDuration(10, SECONDS), () => { 48 | // val followers = friendService.getFollowers("usr2").invoke() 49 | // .toCompletableFuture().get(3, SECONDS) 50 | // assertEquals(Seq("usr1"), followers) 51 | // }) 52 | // 53 | // }) 54 | // } 55 | // 56 | //} 57 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-recommendation-api/src/main/scala/com/scalamicroservices/rec/impl/FriendRecService.scala: -------------------------------------------------------------------------------- 1 | package com.scalamicroservices.rec.impl 2 | 3 | import akka.NotUsed 4 | import com.lightbend.lagom.scaladsl.api.{Descriptor, Service, ServiceCall} 5 | 6 | trait FriendRecService extends Service{ 7 | 8 | def getFriendRecommendation(userId: String): ServiceCall[NotUsed, Seq[String]] 9 | 10 | override def descriptor: Descriptor = { 11 | import Service._ 12 | 13 | named("friendRecService").withCalls( 14 | pathCall("/api/friends/rec/:id", getFriendRecommendation _) 15 | ).withAutoAcl(true) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-recommendation-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = com.scalamicroservices.rec.impl.FriendRecApplicationLoader 2 | 3 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-recommendation-impl/src/main/scala/com/scalamicroservices/rec/impl/FriendRecModule.scala: -------------------------------------------------------------------------------- 1 | package com.scalamicroservices.rec.impl 2 | 3 | import com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaClientComponents 4 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 5 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader, LagomServer} 6 | import com.softwaremill.macwire.wire 7 | import play.api.libs.ws.ahc.AhcWSComponents 8 | import sample.chirper.friend.api.FriendService 9 | 10 | 11 | abstract class FriendRecModule (context: LagomApplicationContext) 12 | extends LagomApplication(context) 13 | with AhcWSComponents 14 | with LagomKafkaClientComponents 15 | { 16 | lazy val friendService: FriendService = serviceClient.implement[FriendService] 17 | override lazy val lagomServer: LagomServer = serverFor[FriendRecService](wire[FriendRecServiceImpl]) 18 | } 19 | 20 | class FriendRecApplicationLoader extends LagomApplicationLoader { 21 | 22 | override def load(context: LagomApplicationContext): LagomApplication = 23 | new FriendRecModule(context) with LagomDevModeComponents 24 | 25 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 26 | new FriendRecModule(context) with LagomDevModeComponents 27 | 28 | override def describeService = Some(readDescriptor[FriendRecService]) 29 | } 30 | -------------------------------------------------------------------------------- /chirper-app-complete/friend-recommendation-impl/src/main/scala/com/scalamicroservices/rec/impl/FriendRecServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package com.scalamicroservices.rec.impl 2 | 3 | import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedQueue} 4 | 5 | import akka.Done 6 | import akka.stream.scaladsl.Flow 7 | import org.slf4j.LoggerFactory 8 | import sample.chirper.friend.api.{FriendService, KFriendAdded, KFriendMessage, KUserCreated} 9 | 10 | import scala.concurrent.{ExecutionContext, Future} 11 | 12 | /** 13 | * This class receives live stream of messages from the topic. The message could be: 14 | * - A new user created or 15 | * - A user has added another user as friend 16 | * 17 | * This class can then model this information so that it can recommend friends when a service call is made. 18 | * It could internally model these friends relationship graphs in its best suited model (may be Neo4J graph database). 19 | * For the sake of simplicity, we calculate this in-memory and respond to users. 20 | */ 21 | class FriendRecServiceImpl(friendService: FriendService)(implicit ex: ExecutionContext) extends FriendRecService { 22 | 23 | val log = LoggerFactory.getLogger(getClass) 24 | 25 | val userMap = new ConcurrentHashMap[String, KUserCreated]() 26 | val allFriends = new ConcurrentLinkedQueue[KFriendAdded]() 27 | 28 | friendService.friendsTopic.subscribe 29 | .atLeastOnce( 30 | Flow[KFriendMessage].map { msg => 31 | log.info("KMessage received " + msg) 32 | msg match { 33 | case x: KUserCreated => userMap.put(x.userId, x) 34 | case y: KFriendAdded => allFriends.add(y) 35 | } 36 | Done 37 | } 38 | ) 39 | 40 | override def getFriendRecommendation(userId: String) = { 41 | request => 42 | val ans = getFriends(userId) 43 | .flatMap(firstLevelFriend => { 44 | getFriends(firstLevelFriend) 45 | }) 46 | .filter(ans => ans != userId) //so that it does not recommend itself 47 | Future.successful(ans) 48 | } 49 | 50 | private def getFriends(userId: String): Seq[String] ={ 51 | import scala.collection.JavaConversions._ 52 | allFriends.filter(all => all.userId == userId).map(x => x.friendId).toList 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /chirper-app-complete/front-end/app/FrontEndLoader.scala: -------------------------------------------------------------------------------- 1 | import com.lightbend.lagom.scaladsl.api.{ServiceAcl, ServiceInfo} 2 | import com.lightbend.lagom.scaladsl.client.LagomServiceClientComponents 3 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 4 | import com.softwaremill.macwire._ 5 | import com.typesafe.conductr.bundlelib.lagom.scaladsl.ConductRApplicationComponents 6 | import play.api.ApplicationLoader.Context 7 | import play.api.i18n.I18nComponents 8 | import play.api.libs.ws.ahc.AhcWSComponents 9 | import play.api.{ApplicationLoader, BuiltInComponentsFromContext, Mode} 10 | import router.Routes 11 | 12 | import scala.collection.immutable 13 | import scala.concurrent.ExecutionContext 14 | 15 | abstract class FrontEndModule(context: Context) extends BuiltInComponentsFromContext(context) 16 | with I18nComponents 17 | with AhcWSComponents 18 | with LagomServiceClientComponents { 19 | 20 | override lazy val serviceInfo: ServiceInfo = ServiceInfo( 21 | "front-end", 22 | Map( 23 | "front-end" -> immutable.Seq(ServiceAcl.forPathRegex("(?!/api/).*")) 24 | ) 25 | ) 26 | 27 | override implicit lazy val executionContext: ExecutionContext = actorSystem.dispatcher 28 | override lazy val router = { 29 | wire[Routes] 30 | } 31 | } 32 | 33 | class FrontEndLoader extends ApplicationLoader { 34 | override def load(context: Context) = context.environment.mode match { 35 | case Mode.Dev => 36 | (new FrontEndModule(context) with LagomDevModeComponents).application 37 | case _ => 38 | (new FrontEndModule(context) with LagomDevModeComponents).application 39 | } 40 | } -------------------------------------------------------------------------------- /chirper-app-complete/front-end/app/controllers/MainController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package controllers 5 | 6 | import play.api.mvc._ 7 | 8 | object MainController extends Controller { 9 | 10 | def index = Action { 11 | Ok(views.html.index.render()) 12 | } 13 | 14 | def userStream(userId: String) = Action { 15 | Ok(views.html.index.render()) 16 | } 17 | 18 | def circuitBreaker = Action { 19 | Ok(views.html.circuitbreaker.render()) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /chirper-app-complete/front-end/app/views/circuitbreaker.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chirper 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chirper-app-complete/front-end/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chirper 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chirper-app-complete/front-end/bundle-configuration/default/runtime-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This shell script is part of the bundle configuration of the `front-end` bundle. 4 | # When loading the `front-end` bundle with a bundle configuration on to ConductR then 5 | # this shell script is executed at the time the `front-end` bundle gets started. 6 | # In this simple script we set the `APPLICATION_SECRET` environment variable 7 | # to override the `play.crypto.secret` key of the `conf/application.conf` 8 | export APPLICATION_SECRET=PPjOW0n2aV?s@6RdiNV@7/5xJhiiKTzk[VdHjkU9YHit8sLHoJ1rp0DCCn6b=lXt 9 | -------------------------------------------------------------------------------- /chirper-app-complete/front-end/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.crypto.secret = "changeme" 2 | play.crypto.secret = ${?APPLICATION_SECRET} 3 | 4 | lagom.play { 5 | service-name = "chirper-front-end" 6 | acls = [ 7 | { 8 | path-regex = "(?!/api/).*" 9 | } 10 | ] 11 | } 12 | 13 | 14 | play.application.loader = FrontEndLoader -------------------------------------------------------------------------------- /chirper-app-complete/front-end/conf/routes: -------------------------------------------------------------------------------- 1 | GET / controllers.MainController.index 2 | GET /signup controllers.MainController.index 3 | GET /addFriend controllers.MainController.index 4 | GET /friendRecommendations controllers.MainController.index 5 | GET /users/:id controllers.MainController.userStream(id) 6 | GET /assets/*file controllers.Assets.versioned(path = "/public", file) 7 | GET /cb controllers.MainController.circuitBreaker 8 | -------------------------------------------------------------------------------- /chirper-app-complete/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /chirper-app-complete/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.3.7") 2 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.1.0") 3 | addSbtPlugin("com.github.ddispaltro" % "sbt-reactjs" % "0.5.2") 4 | addSbtPlugin("com.lightbend.conductr" % "sbt-conductr" % "2.3.0") 5 | -------------------------------------------------------------------------------- /chirper-app-complete/tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lagom Java Chirper 4 | 5 | 6 | 7 |
    8 |

    Welcome to the Lagom Java Chirper Project

    9 | 10 |

    11 | This project demonstrates how to build a twitter-like application, with a few microservices, in Java. For a step by step guide of the project watch the screencasts: 12 |

    18 |

    19 |
    20 | 21 | 22 | -------------------------------------------------------------------------------- /chirper-app-primer/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | -------------------------------------------------------------------------------- /chirper-app-primer/.toDelete: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/chirper-app-primer/.toDelete -------------------------------------------------------------------------------- /chirper-app-primer/README.md: -------------------------------------------------------------------------------- 1 | A twitter like application built using Lightbend Lagom. 2 | This is built on top of the exising [lagom-scala-chirper](https://github.com/dotta/activator-lagom-scala-chirper) by the awesome [Mirco Dotta](https://twitter.com/mircodotta). It contains a few changes when compared to original repo: 3 | * It uses the scaladsl api instead of javadsl from scala 4 | * For the sake of learning. The code is bare minimum and only uses Lagom service api related concepts. Which means it: 5 | * It does not use the Persistence API. All the data is simply stored on-memory using Map 6 | * It neither uses the Message Broker API 7 | 8 | Like it says: bare minimum 9 | 10 | ## Run 11 | 12 | Start all services using `sbt runAll`. To access the application post the startup.: [http://localhost:9000](http://localhost:9000) 13 | * You can then signup 14 | * Add a friend 15 | * And chirp 16 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-api/src/main/scala/sample/chirper/activity/api/ActivityStreamService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.api 5 | 6 | import sample.chirper.chirp.api.Chirp 7 | 8 | import akka.stream.scaladsl.Source 9 | 10 | import akka.NotUsed 11 | import com.lightbend.lagom.scaladsl.api.ServiceCall 12 | import com.lightbend.lagom.scaladsl.api.Descriptor 13 | import com.lightbend.lagom.scaladsl.api.Service 14 | 15 | trait ActivityStreamService extends Service { 16 | 17 | def getLiveActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] 18 | 19 | def getHistoricalActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] 20 | 21 | override def descriptor(): Descriptor = { 22 | import Service._ 23 | 24 | named("activityservice").withCalls( 25 | pathCall("/api/activity/:userId/live", getLiveActivityStream _), 26 | pathCall("/api/activity/:userId/history", getHistoricalActivityStream _) 27 | ).withAutoAcl(true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-api/src/main/scala/sample/chirper/activity/api/HistoricalActivityStreamReq.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.api 5 | 6 | import java.time.Instant 7 | 8 | case class HistoricalActivityStreamReq(fromTime: Instant) 9 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.activity.impl.ActivityStreamApplicationLoader 2 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-impl/src/main/scala/sample/chirper/activity/impl/ActivityStreamModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.impl 5 | 6 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 7 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader} 8 | import com.softwaremill.macwire._ 9 | import play.api.libs.ws.ahc.AhcWSComponents 10 | import sample.chirper.activity.api.ActivityStreamService 11 | import sample.chirper.chirp.api.ChirpService 12 | import sample.chirper.friend.api.FriendService 13 | 14 | 15 | abstract class ActivityStreamModule (context: LagomApplicationContext) 16 | extends LagomApplication(context) 17 | with AhcWSComponents { 18 | lazy val friendService: FriendService = serviceClient.implement[FriendService] 19 | lazy val chirpService: ChirpService = serviceClient.implement[ChirpService] 20 | 21 | override lazy val lagomServer = serverFor[ActivityStreamService](wire[ActivityStreamServiceImpl]) 22 | } 23 | 24 | 25 | class ActivityStreamApplicationLoader extends LagomApplicationLoader { 26 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 27 | new ActivityStreamModule(context) with LagomDevModeComponents 28 | 29 | override def load(context: LagomApplicationContext): LagomApplication = 30 | // new FriendModule(context) with ConductRApplicationComponents 31 | new ActivityStreamModule(context) with LagomDevModeComponents 32 | 33 | override def describeService = Some(readDescriptor[FriendService]) 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /chirper-app-primer/activity-stream-impl/src/main/scala/sample/chirper/activity/impl/ActivityStreamServiceImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.activity.impl 5 | 6 | import java.time.Duration 7 | import java.time.Instant 8 | 9 | import scala.compat.java8.FutureConverters._ 10 | import scala.concurrent.ExecutionContext 11 | 12 | import com.lightbend.lagom.scaladsl.api.ServiceCall 13 | 14 | import akka.NotUsed 15 | import akka.stream.scaladsl.Source 16 | 17 | import sample.chirper.activity.api.ActivityStreamService 18 | import sample.chirper.chirp.api.Chirp 19 | import sample.chirper.chirp.api.ChirpService 20 | import sample.chirper.chirp.api.HistoricalChirpsRequest 21 | import sample.chirper.chirp.api.LiveChirpsRequest 22 | import sample.chirper.friend.api.FriendService 23 | 24 | class ActivityStreamServiceImpl ( 25 | friendService: FriendService, 26 | chirpService: ChirpService)(implicit ec: ExecutionContext) extends ActivityStreamService { 27 | 28 | 29 | override def getLiveActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] = { 30 | req => 31 | for { 32 | user <- friendService.getUser(userId).invoke() 33 | userIds = user.friends :+ userId 34 | chirpsReq = LiveChirpsRequest(userIds) 35 | chirps <- chirpService.getLiveChirps.invoke(chirpsReq) 36 | } yield chirps 37 | } 38 | 39 | override def getHistoricalActivityStream(userId: String): ServiceCall[NotUsed, Source[Chirp, NotUsed]] = { 40 | req => 41 | for { 42 | user <- friendService.getUser(userId).invoke() 43 | userIds = user.friends :+ userId 44 | // FIXME we should use HistoricalActivityStreamReq request parameter 45 | fromTime = Instant.now().minus(Duration.ofDays(7)) 46 | chirpsReq = HistoricalChirpsRequest(fromTime, userIds) 47 | chirps <- chirpService.getHistoricalChirps.invoke(chirpsReq) 48 | } yield chirps 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /chirper-app-primer/build.sbt: -------------------------------------------------------------------------------- 1 | name := "chriper-app-primer" 2 | 3 | organization in ThisBuild := "sample.chirper" 4 | 5 | scalaVersion in ThisBuild := "2.11.8" 6 | 7 | val macwire = "com.softwaremill.macwire" %% "macros" % "2.2.5" % "provided" 8 | 9 | lazy val friendApi = project("friend-api") 10 | .settings( 11 | version := "1.0-SNAPSHOT", 12 | libraryDependencies += lagomScaladslApi 13 | ) 14 | 15 | lazy val friendImpl = project("friend-impl") 16 | .enablePlugins(LagomScala) 17 | .settings( 18 | version := "1.0-SNAPSHOT", 19 | libraryDependencies ++= Seq( 20 | lagomScaladslTestKit, 21 | macwire 22 | ) 23 | ) 24 | .settings(lagomForkedTestSettings: _*) 25 | .dependsOn(friendApi) 26 | 27 | lazy val chirpApi = project("chirp-api") 28 | .settings( 29 | version := "1.0-SNAPSHOT", 30 | libraryDependencies ++= Seq( 31 | lagomScaladslApi 32 | ) 33 | ) 34 | 35 | lazy val chirpImpl = project("chirp-impl") 36 | .enablePlugins(LagomScala) 37 | .settings( 38 | version := "1.0-SNAPSHOT", 39 | libraryDependencies ++= Seq( 40 | lagomScaladslPubSub, 41 | lagomScaladslTestKit, 42 | macwire 43 | ) 44 | ) 45 | .settings(lagomForkedTestSettings: _*) 46 | .dependsOn(chirpApi) 47 | 48 | lazy val activityStreamApi = project("activity-stream-api") 49 | .settings( 50 | version := "1.0-SNAPSHOT", 51 | libraryDependencies += lagomScaladslApi 52 | ) 53 | .dependsOn(chirpApi) 54 | 55 | lazy val activityStreamImpl = project("activity-stream-impl") 56 | .enablePlugins(LagomScala) 57 | .settings( 58 | version := "1.0-SNAPSHOT", 59 | libraryDependencies ++= Seq( 60 | lagomScaladslTestKit, 61 | macwire 62 | ) 63 | ) 64 | .dependsOn(activityStreamApi, chirpApi, friendApi) 65 | 66 | lazy val frontEnd = project("front-end") 67 | .enablePlugins(PlayScala, LagomPlay) 68 | .settings( 69 | version := "1.0-SNAPSHOT", 70 | routesGenerator := StaticRoutesGenerator, 71 | libraryDependencies ++= Seq( 72 | "org.webjars" % "react" % "0.14.3", 73 | "org.webjars" % "react-router" % "1.0.3", 74 | "org.webjars" % "jquery" % "2.2.0", 75 | "org.webjars" % "foundation" % "5.3.0", 76 | macwire, 77 | lagomScaladslServer 78 | ), 79 | ReactJsKeys.sourceMapInline := true 80 | ) 81 | 82 | def project(id: String) = Project(id, base = file(id)) 83 | .settings( 84 | scalacOptions in Compile += "-Xexperimental" // this enables Scala lambdas to be passed as Scala SAMs 85 | ) 86 | .settings( 87 | libraryDependencies ++= Seq( 88 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.7.3" // actually, only api projects need this 89 | ) 90 | ) 91 | 92 | licenses in ThisBuild := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) 93 | 94 | lagomCassandraEnabled in ThisBuild := false 95 | 96 | lagomKafkaEnabled in ThisBuild := false -------------------------------------------------------------------------------- /chirper-app-primer/chirp-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-primer/chirp-api/src/main/scala/sample/chirper/chirp/api/Chirp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import java.time.Instant 7 | import java.util.UUID 8 | 9 | import com.fasterxml.jackson.annotation.JsonIgnore 10 | import play.api.libs.functional.syntax._ 11 | import play.api.libs.json.{JsPath, Json, Reads} // Combinator syntax 12 | 13 | case class Chirp (userId: String, message: String, @JsonIgnore timestamp: Instant,@JsonIgnore uuid: String) { 14 | def this(userId: String, message: String) = 15 | this(userId, message, Chirp.defaultTimestamp, Chirp.defaultUUID) 16 | } 17 | 18 | object Chirp { 19 | implicit object ChirpOrdering extends Ordering[Chirp] { 20 | override def compare(x: Chirp, y: Chirp): Int = x.timestamp.compareTo(y.timestamp) 21 | } 22 | 23 | def apply(userId: String, message: String, timestamp: Option[Instant], uuid: Option[String]): Chirp = 24 | new Chirp(userId, message, timestamp.getOrElse(defaultTimestamp), uuid.getOrElse(defaultUUID)) 25 | 26 | private def defaultTimestamp = Instant.now() 27 | private def defaultUUID = UUID.randomUUID().toString 28 | 29 | implicit val chirpRead: Reads[Chirp] = ( 30 | (JsPath \ "userId").read[String] and 31 | (JsPath \ "message").read[String] 32 | )((userId, message) => Chirp(userId, message, None, None)) 33 | 34 | implicit val chirpWrite = Json.writes[Chirp] 35 | } -------------------------------------------------------------------------------- /chirper-app-primer/chirp-api/src/main/scala/sample/chirper/chirp/api/ChirpService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import akka.stream.scaladsl.Source 7 | import akka.{Done, NotUsed} 8 | import com.lightbend.lagom.scaladsl.api.Descriptor 9 | import com.lightbend.lagom.scaladsl.api.Service 10 | import com.lightbend.lagom.scaladsl.api.ServiceCall 11 | 12 | trait ChirpService extends Service { 13 | 14 | def addChirp(userId: String): ServiceCall[Chirp, Done] 15 | 16 | def getLiveChirps: ServiceCall[LiveChirpsRequest, Source[Chirp, NotUsed]] 17 | 18 | def getHistoricalChirps: ServiceCall[HistoricalChirpsRequest, Source[Chirp, NotUsed]] 19 | 20 | override def descriptor(): Descriptor = { 21 | import Service._ 22 | 23 | named("chirpservice").withCalls( 24 | pathCall("/api/chirps/live/:userId", addChirp _), 25 | namedCall("/api/chirps/history", getHistoricalChirps _), 26 | namedCall("/api/chirps/live", getLiveChirps _) 27 | ).withAutoAcl(true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chirper-app-primer/chirp-api/src/main/scala/sample/chirper/chirp/api/HistoricalChirpsRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import java.time.Instant 7 | 8 | import play.api.libs.json.Json 9 | 10 | import scala.collection.immutable.Seq 11 | 12 | case class HistoricalChirpsRequest(fromTime: Instant, userIds: Seq[String]) 13 | 14 | object HistoricalChirpsRequest{ 15 | implicit val format = Json.format[HistoricalChirpsRequest] 16 | } 17 | -------------------------------------------------------------------------------- /chirper-app-primer/chirp-api/src/main/scala/sample/chirper/chirp/api/LiveChirpsRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.api 5 | 6 | import play.api.libs.json.Json 7 | 8 | import scala.collection.immutable.Seq 9 | 10 | case class LiveChirpsRequest(userIds: Seq[String]) 11 | 12 | object LiveChirpsRequest{ 13 | implicit val liveChirpsRequestFormat = Json.format[LiveChirpsRequest] 14 | } 15 | -------------------------------------------------------------------------------- /chirper-app-primer/chirp-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.chirp.impl.ChirpApplicationLoader -------------------------------------------------------------------------------- /chirper-app-primer/chirp-impl/src/main/scala/sample/chirper/chirp/impl/ChipServiceImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.impl 5 | 6 | import akka.{Done, NotUsed} 7 | import akka.stream.scaladsl.Source 8 | import com.lightbend.lagom.scaladsl.api.ServiceCall 9 | 10 | import com.lightbend.lagom.scaladsl.pubsub.{PubSubRef, PubSubRegistry, TopicId} 11 | import org.slf4j.LoggerFactory 12 | import sample.chirper.chirp.api.{Chirp, ChirpService, HistoricalChirpsRequest, LiveChirpsRequest} 13 | 14 | import scala.collection.immutable.Seq 15 | import scala.concurrent.{ExecutionContext, Future} 16 | 17 | object ChirpServiceImpl { 18 | final val MaxTopics = 1024 19 | } 20 | 21 | class ChirpServiceImpl (topics: PubSubRegistry)(implicit ex: ExecutionContext) extends ChirpService { 22 | 23 | 24 | private val log = LoggerFactory.getLogger(classOf[ChirpServiceImpl]) 25 | 26 | /** 27 | * By default sorted with time 28 | */ 29 | private var allChirps = List[Chirp]() 30 | 31 | override def addChirp(userId: String): ServiceCall[Chirp, Done] = { 32 | chirp => { 33 | if (userId != chirp.userId) 34 | throw new IllegalArgumentException(s"UserId $userId did not match userId in $chirp") 35 | 36 | val topic: PubSubRef[Chirp] = topics.refFor(TopicId(topicQualifier(userId))) 37 | topic.publish(chirp) 38 | 39 | this.synchronized { 40 | allChirps = chirp :: allChirps 41 | } 42 | Future.successful(Done) 43 | } 44 | } 45 | 46 | private def topicQualifier(userId: String): String = 47 | String.valueOf(Math.abs(userId.hashCode()) % ChirpServiceImpl.MaxTopics) 48 | 49 | override def getLiveChirps: ServiceCall[LiveChirpsRequest, Source[Chirp, NotUsed]] = { 50 | req => { 51 | recentChirps(req.userIds).map { chirps => 52 | val sources: Seq[Source[Chirp, NotUsed]] = for(userId <- req.userIds) yield { 53 | val topic: PubSubRef[Chirp] = topics.refFor(TopicId(topicQualifier(userId))) 54 | topic.subscriber 55 | } 56 | 57 | val users = req.userIds.toSet 58 | val publishedChirps = Source(sources).flatMapMerge(sources.size, x => x) 59 | .filter(chirp => users(chirp.userId)) 60 | 61 | // We currently ignore the fact that it is possible to get duplicate chirps 62 | // from the recent and the topic. That can be solved with a de-duplication stage. 63 | Source(chirps).concat(publishedChirps) 64 | } 65 | } 66 | } 67 | 68 | override def getHistoricalChirps: ServiceCall[HistoricalChirpsRequest, Source[Chirp, NotUsed]] = { 69 | req => { 70 | val userIds = req.userIds 71 | val chirps = userIds.map { userId => 72 | Source((for { 73 | row <- allChirps 74 | if row.userId == userId 75 | } yield row).sortWith((a, b) => a.timestamp.compareTo(b.timestamp) <= 0)) 76 | } 77 | 78 | // Chirps from one user are ordered by timestamp, but chirps from different 79 | // users are not ordered. That can be improved by implementing a smarter 80 | // merge that takes the timestamps into account. 81 | val x = Source(chirps) 82 | val res = x.flatMapMerge(chirps.size, x => x) 83 | Future.successful(res) 84 | } 85 | } 86 | 87 | 88 | private def recentChirps(userIds: Seq[String]): Future[Seq[Chirp]] = { 89 | val limit = 10 90 | def getChirps(userId: String): Future[Seq[Chirp]] = { 91 | Future.successful(for { 92 | row <- allChirps 93 | if row.userId == userId 94 | } yield row) 95 | } 96 | val results = Future.sequence(userIds.map(getChirps)).map(_.flatten) 97 | val sortedLimited = results.map(_.sorted.reverse) // reverse order 98 | 99 | sortedLimited.map(_.take(limit)) // take only latest chirps 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /chirper-app-primer/chirp-impl/src/main/scala/sample/chirper/chirp/impl/ChirpModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.chirp.impl 5 | 6 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 7 | import com.lightbend.lagom.scaladsl.pubsub.PubSubComponents 8 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader} 9 | import com.softwaremill.macwire._ 10 | import play.api.libs.ws.ahc.AhcWSComponents 11 | import sample.chirper.chirp.api.ChirpService 12 | 13 | abstract class ChirpModule(context: LagomApplicationContext) 14 | extends LagomApplication(context) 15 | with AhcWSComponents 16 | with PubSubComponents 17 | { 18 | override lazy val lagomServer = serverFor[ChirpService](wire[ChirpServiceImpl]) 19 | } 20 | 21 | class ChirpApplicationLoader extends LagomApplicationLoader { 22 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 23 | new ChirpModule(context) with LagomDevModeComponents 24 | 25 | override def load(context: LagomApplicationContext): LagomApplication = 26 | // new ChirpModule(context) with ConductRApplicationComponents 27 | new ChirpModule(context) with LagomDevModeComponents 28 | 29 | override def describeService = Some(readDescriptor[ChirpService]) 30 | } -------------------------------------------------------------------------------- /chirper-app-primer/chirp-impl/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-api/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | lagom.serialization.json.jackson-modules += com.fasterxml.jackson.module.scala.DefaultScalaModule 2 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-api/src/main/scala/sample/chirper/friend/api/FriendId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import play.api.libs.json.Json 7 | 8 | case class FriendId(friendId: String) 9 | 10 | object FriendId { 11 | implicit val friendIdJson = Json.format[FriendId] 12 | } 13 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-api/src/main/scala/sample/chirper/friend/api/FriendService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import akka.{Done, NotUsed} 7 | import com.lightbend.lagom.scaladsl.api.Descriptor 8 | import com.lightbend.lagom.scaladsl.api.Service 9 | import com.lightbend.lagom.scaladsl.api.ServiceCall 10 | 11 | import scala.collection.immutable.Seq 12 | 13 | 14 | /** 15 | * The friend service. 16 | */ 17 | trait FriendService extends Service { 18 | 19 | /** 20 | * Service call for getting a user. 21 | * 22 | * The ID of this service call is the user name, and the response message is the User object. 23 | */ 24 | def getUser(id: String): ServiceCall[NotUsed, User] 25 | 26 | /** 27 | * Service call for creating a user. 28 | * 29 | * The request message is the User to create. 30 | */ 31 | def createUser(): ServiceCall[CreateUser, Done] 32 | 33 | /** 34 | * Service call for adding a friend to a user. 35 | * 36 | * The ID for this service call is the ID of the user that the friend is being added to. 37 | * The request message is the ID of the friend being added. 38 | */ 39 | def addFriend(userId: String): ServiceCall[FriendId, Done] 40 | 41 | /** 42 | * Service call for getting the followers of a user. 43 | * 44 | * The ID for this service call is the Id of the user to get the followers for. 45 | * The response message is the list of follower IDs. 46 | */ 47 | def getFollowers(userId: String): ServiceCall[NotUsed, Seq[String]] 48 | 49 | override def descriptor(): Descriptor = { 50 | import Service._ 51 | 52 | named("friendservice").withCalls( 53 | pathCall("/api/users/:id", getUser _), 54 | namedCall("/api/users", createUser), 55 | pathCall("/api/users/:userId/friends", addFriend _), 56 | pathCall("/api/users/:id/followers", getFollowers _) 57 | ).withAutoAcl(true) 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-api/src/main/scala/sample/chirper/friend/api/User.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.api 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnore 7 | import play.api.libs.json.Json 8 | 9 | import scala.collection.immutable.Seq 10 | 11 | case class CreateUser (userId: String, name:String) 12 | 13 | object CreateUser{ 14 | implicit val createUserJson = Json.format[CreateUser] 15 | } 16 | 17 | case class User (userId: String, name: String, friends: Seq[String]) { 18 | def this(userId: String, name: String) = this(userId, name, Seq.empty) 19 | } 20 | 21 | object User { 22 | implicit val userJson = Json.format[User] 23 | 24 | def apply(createUser: CreateUser):User = User(createUser.userId, createUser.name, Seq()) 25 | // def apply (userId: String, name: String, friends: Seq[String]) = new User(userId, name, friends) 26 | } 27 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | play.application.loader = sample.chirper.friend.impl.FriendApplicationLoader 2 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | 7 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 8 | import com.lightbend.lagom.scaladsl.server.{LagomApplication, LagomApplicationContext, LagomApplicationLoader, LagomServer} 9 | import com.softwaremill.macwire.wire 10 | import play.api.libs.ws.ahc.AhcWSComponents 11 | import sample.chirper.friend.api.FriendService 12 | 13 | abstract class FriendModule (context: LagomApplicationContext) extends LagomApplication(context) with AhcWSComponents { 14 | override lazy val lagomServer: LagomServer = serverFor[FriendService](wire[FriendServiceImpl]) 15 | } 16 | 17 | class FriendApplicationLoader extends LagomApplicationLoader { 18 | 19 | override def load(context: LagomApplicationContext): LagomApplication = 20 | new FriendModule(context) with LagomDevModeComponents 21 | 22 | override def loadDevMode(context: LagomApplicationContext): LagomApplication = 23 | new FriendModule(context) with LagomDevModeComponents 24 | 25 | override def describeService = Some(readDescriptor[FriendService]) 26 | } 27 | 28 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/main/scala/sample/chirper/friend/impl/FriendServiceImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedQueue} 7 | 8 | import akka.{Done, NotUsed} 9 | import com.lightbend.lagom.scaladsl.api.ServiceCall 10 | import com.lightbend.lagom.scaladsl.api.transport.NotFound 11 | import sample.chirper.friend.api.{CreateUser, FriendId, FriendService, User} 12 | 13 | import scala.collection.immutable.Seq 14 | import scala.concurrent.{ExecutionContext, Future} 15 | 16 | class FriendServiceImpl()(implicit ec: ExecutionContext) extends FriendService { 17 | 18 | val userMap = new ConcurrentHashMap[String, User]() 19 | 20 | val friendsMap = new ConcurrentHashMap[String, ConcurrentLinkedQueue[User]]() 21 | 22 | override def getUser(id: String): ServiceCall[NotUsed, User] = { 23 | request => 24 | val user = userMap.get(id) 25 | if (user == null) 26 | throw NotFound(s"user $id not found") 27 | else { 28 | Future.successful(getUser(user.userId, user.name)) 29 | } 30 | } 31 | 32 | override def createUser(): ServiceCall[CreateUser, Done] = { 33 | request => 34 | this.synchronized { 35 | val alreadyExists = userMap.get(request.userId) 36 | if (alreadyExists != null) { 37 | throw NotFound(s"user $request already exists") 38 | } 39 | 40 | val user = User(request) 41 | userMap.put(request.userId, user) 42 | val friends = new ConcurrentLinkedQueue[User]() 43 | 44 | friendsMap.put(user.userId, friends) 45 | Future.successful(Done) 46 | } 47 | } 48 | 49 | override def addFriend(userId: String): ServiceCall[FriendId, Done] = { 50 | request => 51 | val user = userMap.get(userId) 52 | 53 | if (user == null) 54 | throw NotFound(s"user $userId not found") 55 | else { 56 | val friendsList = friendsMap.get(userId) 57 | val friend = userMap.get(request.friendId) 58 | friendsList.add(friend) 59 | Future.successful(Done) 60 | } 61 | } 62 | 63 | 64 | override def getFollowers(id: String): ServiceCall[NotUsed, Seq[String]] = { 65 | req => { 66 | val user = userMap.get(id) 67 | if (user == null) 68 | throw NotFound(s"user $id not found") 69 | else { 70 | Future.successful(getUser(user.userId, user.name).friends) 71 | } 72 | } 73 | } 74 | 75 | private def getUser(userId: String, name: String): User = { 76 | import scala.collection.JavaConverters._ 77 | 78 | User(userId, name, friendsMap.get(userId).asScala.toList.map(x => x.userId)) 79 | } 80 | } -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date{ISO8601} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/test/scala/sample/chirper/friend/impl/FriendEntityTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package sample.chirper.friend.impl 5 | 6 | import java.util.Collections 7 | 8 | import scala.collection.immutable.Seq 9 | 10 | import org.junit.AfterClass 11 | import org.junit.Assert.assertEquals 12 | import org.junit.BeforeClass 13 | import org.junit.Test 14 | 15 | import com.lightbend.lagom.scaladsl.persistence.PersistentEntity 16 | import com.lightbend.lagom.scaladsl.testkit.PersistentEntityTestDriver 17 | 18 | import akka.Done 19 | import akka.actor.ActorSystem 20 | import akka.testkit.JavaTestKit 21 | import sample.chirper.friend.api.User 22 | 23 | 24 | object FriendEntityTest { 25 | @volatile private var system: ActorSystem = _ 26 | @BeforeClass 27 | def setup(): Unit = { 28 | system = ActorSystem.create("FriendEntityTest") 29 | } 30 | 31 | @AfterClass 32 | def teardown(): Unit = { 33 | JavaTestKit.shutdownActorSystem(system) 34 | system = null 35 | } 36 | } 37 | 38 | class FriendEntityTest { 39 | 40 | import FriendEntityTest.system 41 | @Test 42 | def testCreateUser(): Unit = { 43 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 44 | 45 | val outcome = driver.run(CreateUser(new User("alice", "Alice"))) 46 | assertEquals(Done, outcome.getReplies.get(0)) 47 | assertEquals("alice", outcome.events.get(0).asInstanceOf[UserCreated].userId) 48 | assertEquals("Alice", outcome.events.get(0).asInstanceOf[UserCreated].name) 49 | assertEquals(Collections.emptyList(), driver.getAllIssues) 50 | } 51 | 52 | @Test 53 | def testRejectDuplicateCreate(): Unit = { 54 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 55 | driver.run(new CreateUser(new User("alice", "Alice"))); 56 | 57 | val outcome = driver.run(CreateUser(new User("alice", "Alice"))) 58 | assertEquals(classOf[PersistentEntity.InvalidCommandException], outcome.getReplies.get(0).getClass()) 59 | assertEquals(Collections.emptyList(), outcome.events) 60 | assertEquals(Collections.emptyList(), driver.getAllIssues) 61 | } 62 | 63 | @Test 64 | def testCreateUserWithInitialFriends(): Unit = { 65 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 66 | 67 | val friends = Seq("bob", "peter") 68 | val outcome = driver.run(CreateUser(User("alice", "Alice", friends))) 69 | assertEquals(Done, outcome.getReplies.get(0)) 70 | assertEquals("alice", outcome.events.get(0).asInstanceOf[UserCreated].userId) 71 | assertEquals("bob", outcome.events.get(1).asInstanceOf[FriendAdded].friendId) 72 | assertEquals("peter", outcome.events.get(2).asInstanceOf[FriendAdded].friendId) 73 | assertEquals(Collections.emptyList(), driver.getAllIssues) 74 | } 75 | 76 | @Test 77 | def testAddFriend(): Unit = { 78 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 79 | driver.run(CreateUser(new User("alice", "Alice"))) 80 | 81 | val outcome = driver.run(AddFriend("bob"), AddFriend("peter")) 82 | assertEquals(Done, outcome.getReplies.get(0)) 83 | assertEquals("bob", outcome.events.get(0).asInstanceOf[FriendAdded].friendId) 84 | assertEquals("peter", outcome.events.get(1).asInstanceOf[FriendAdded].friendId) 85 | assertEquals(Collections.emptyList(), driver.getAllIssues) 86 | } 87 | 88 | @Test 89 | def testAddDuplicateFriend(): Unit = { 90 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 91 | driver.run(CreateUser(new User("alice", "Alice"))) 92 | driver.run(AddFriend("bob"), AddFriend("peter")) 93 | 94 | val outcome = driver.run(AddFriend("bob")) 95 | assertEquals(Done, outcome.getReplies.get(0)) 96 | assertEquals(Collections.emptyList(), outcome.events) 97 | assertEquals(Collections.emptyList(), driver.getAllIssues) 98 | } 99 | 100 | @Test 101 | def testGetUser(): Unit = { 102 | val driver = new PersistentEntityTestDriver(system, new FriendEntity(), "user-1") 103 | val alice = new User("alice", "Alice") 104 | driver.run(CreateUser(alice)) 105 | 106 | val outcome = driver.run(GetUser()) 107 | assertEquals(GetUserReply(Some(alice)), outcome.getReplies.get(0)) 108 | assertEquals(Collections.emptyList(), outcome.events) 109 | assertEquals(Collections.emptyList(), driver.getAllIssues) 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /chirper-app-primer/friend-impl/src/test/scala/sample/chirper/friend/impl/FriendServiceTest.scala: -------------------------------------------------------------------------------- 1 | ///* 2 | // * Copyright (C) 2016 Lightbend Inc. 3 | // */ 4 | //package sample.chirper.friend.impl 5 | // 6 | //import java.util.concurrent.TimeUnit.SECONDS 7 | // 8 | //import scala.collection.immutable.Seq 9 | //import scala.concurrent.duration.FiniteDuration 10 | // 11 | //import org.junit.Assert.assertEquals 12 | //import org.junit.Test 13 | // 14 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.defaultSetup 15 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.eventually 16 | //import com.lightbend.lagom.scaladsl.testkit.ServiceTest.withServer 17 | // 18 | //import akka.NotUsed 19 | //import sample.chirper.friend.api.FriendId 20 | //import sample.chirper.friend.api.FriendService 21 | //import sample.chirper.friend.api.User 22 | // 23 | //class FriendServiceTest { 24 | // 25 | // 26 | // @throws(classOf[Exception]) 27 | // @Test 28 | // def shouldBeAbleToCreateUsersAndConnectFriends() { 29 | // withServer(defaultSetup, server => { 30 | // val friendService = server.client(classOf[FriendService]) 31 | // val usr1 = new User("usr1", "User 1"); 32 | // friendService.createUser().invoke(usr1).toCompletableFuture().get(10, SECONDS) 33 | // val usr2 = new User("usr2", "User 2"); 34 | // friendService.createUser().invoke(usr2).toCompletableFuture().get(3, SECONDS) 35 | // val usr3 = new User("usr3", "User 3"); 36 | // friendService.createUser().invoke(usr3).toCompletableFuture().get(3, SECONDS) 37 | // 38 | // friendService.addFriend("usr1").invoke(FriendId(usr2.userId)).toCompletableFuture().get(3, SECONDS) 39 | // friendService.addFriend("usr1").invoke(FriendId(usr3.userId)).toCompletableFuture().get(3, SECONDS) 40 | // 41 | // val fetchedUsr1 = friendService.getUser("usr1").invoke(NotUsed).toCompletableFuture().get(3, 42 | // SECONDS) 43 | // assertEquals(usr1.userId, fetchedUsr1.userId) 44 | // assertEquals(usr1.name, fetchedUsr1.name) 45 | // assertEquals(Seq("usr2", "usr3"), fetchedUsr1.friends) 46 | // 47 | // eventually(FiniteDuration(10, SECONDS), () => { 48 | // val followers = friendService.getFollowers("usr2").invoke() 49 | // .toCompletableFuture().get(3, SECONDS) 50 | // assertEquals(Seq("usr1"), followers) 51 | // }) 52 | // 53 | // }) 54 | // } 55 | // 56 | //} 57 | -------------------------------------------------------------------------------- /chirper-app-primer/front-end/app/FrontEndLoader.scala: -------------------------------------------------------------------------------- 1 | import com.lightbend.lagom.scaladsl.api.{ServiceAcl, ServiceInfo} 2 | import com.lightbend.lagom.scaladsl.client.LagomServiceClientComponents 3 | import com.lightbend.lagom.scaladsl.devmode.LagomDevModeComponents 4 | import com.softwaremill.macwire._ 5 | import com.typesafe.conductr.bundlelib.lagom.scaladsl.ConductRApplicationComponents 6 | import play.api.ApplicationLoader.Context 7 | import play.api.i18n.I18nComponents 8 | import play.api.libs.ws.ahc.AhcWSComponents 9 | import play.api.{ApplicationLoader, BuiltInComponentsFromContext, Mode} 10 | import router.Routes 11 | 12 | import scala.collection.immutable 13 | import scala.concurrent.ExecutionContext 14 | 15 | abstract class FrontEndModule(context: Context) extends BuiltInComponentsFromContext(context) 16 | with I18nComponents 17 | with AhcWSComponents 18 | with LagomServiceClientComponents { 19 | 20 | override lazy val serviceInfo: ServiceInfo = ServiceInfo( 21 | "front-end", 22 | Map( 23 | "front-end" -> immutable.Seq(ServiceAcl.forPathRegex("(?!/api/).*")) 24 | ) 25 | ) 26 | 27 | override implicit lazy val executionContext: ExecutionContext = actorSystem.dispatcher 28 | override lazy val router = { 29 | wire[Routes] 30 | } 31 | } 32 | 33 | class FrontEndLoader extends ApplicationLoader { 34 | override def load(context: Context) = context.environment.mode match { 35 | case Mode.Dev => 36 | (new FrontEndModule(context) with LagomDevModeComponents).application 37 | case _ => 38 | (new FrontEndModule(context) with LagomDevModeComponents).application 39 | } 40 | } -------------------------------------------------------------------------------- /chirper-app-primer/front-end/app/controllers/MainController.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package controllers 5 | 6 | import play.api.mvc._ 7 | 8 | object MainController extends Controller { 9 | 10 | def index = Action { 11 | Ok(views.html.index.render()) 12 | } 13 | 14 | def userStream(userId: String) = Action { 15 | Ok(views.html.index.render()) 16 | } 17 | 18 | def circuitBreaker = Action { 19 | Ok(views.html.circuitbreaker.render()) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /chirper-app-primer/front-end/app/views/circuitbreaker.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chirper 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chirper-app-primer/front-end/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chirper 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /chirper-app-primer/front-end/bundle-configuration/default/runtime-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This shell script is part of the bundle configuration of the `front-end` bundle. 4 | # When loading the `front-end` bundle with a bundle configuration on to ConductR then 5 | # this shell script is executed at the time the `front-end` bundle gets started. 6 | # In this simple script we set the `APPLICATION_SECRET` environment variable 7 | # to override the `play.crypto.secret` key of the `conf/application.conf` 8 | export APPLICATION_SECRET=PPjOW0n2aV?s@6RdiNV@7/5xJhiiKTzk[VdHjkU9YHit8sLHoJ1rp0DCCn6b=lXt 9 | -------------------------------------------------------------------------------- /chirper-app-primer/front-end/conf/application.conf: -------------------------------------------------------------------------------- 1 | play.crypto.secret = "changeme" 2 | play.crypto.secret = ${?APPLICATION_SECRET} 3 | 4 | lagom.play { 5 | service-name = "chirper-front-end" 6 | acls = [ 7 | { 8 | path-regex = "(?!/api/).*" 9 | } 10 | ] 11 | } 12 | 13 | 14 | play.application.loader = FrontEndLoader -------------------------------------------------------------------------------- /chirper-app-primer/front-end/conf/routes: -------------------------------------------------------------------------------- 1 | GET / controllers.MainController.index 2 | GET /signup controllers.MainController.index 3 | GET /addFriend controllers.MainController.index 4 | GET /users/:id controllers.MainController.userStream(id) 5 | GET /assets/*file controllers.Assets.versioned(path = "/public", file) 6 | GET /cb controllers.MainController.circuitBreaker 7 | -------------------------------------------------------------------------------- /chirper-app-primer/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /chirper-app-primer/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.3.7") 2 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.1.0") 3 | addSbtPlugin("com.github.ddispaltro" % "sbt-reactjs" % "0.5.2") 4 | addSbtPlugin("com.lightbend.conductr" % "sbt-conductr" % "2.3.0") 5 | -------------------------------------------------------------------------------- /chirper-app-primer/tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lagom Java Chirper 4 | 5 | 6 | 7 |
    8 |

    Welcome to the Lagom Java Chirper Project

    9 | 10 |

    11 | This project demonstrates how to build a twitter-like application, with a few microservices, in Java. For a step by step guide of the project watch the screencasts: 12 |

    18 |

    19 |
    20 | 21 | 22 | -------------------------------------------------------------------------------- /first-app/app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | import javax.inject.Singleton 5 | 6 | import play.api.mvc._ 7 | import play.twirl.api.Html 8 | 9 | import scala.util.{Failure, Success, Try} 10 | 11 | @Singleton 12 | class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) { 13 | 14 | def index() = Action { 15 | Ok("Hello World!") 16 | .withHeaders("Server" -> "Play") 17 | .withCookies(Cookie("id", scala.util.Random.nextInt().toString)) 18 | } 19 | 20 | def sqrt(num: String) = Action { 21 | Try(num.toInt) match { 22 | case Success(ans) if ans >= 0 => Ok(s"The answer is: ${math.sqrt(ans)}") 23 | case Success(ans) => BadRequest(s"The input ($num) must be greater than zero") 24 | case Failure(ex) => InternalServerError(s"Could not extract the contents from $num") 25 | } 26 | } 27 | 28 | 29 | def hello(name: String) = Action { 30 | val c: Html = views.html.index(name) 31 | Ok(c) 32 | // Ok("Hello "+name) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /first-app/app/controllers/LoginController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | import javax.inject.Singleton 5 | 6 | import play.api.mvc._ 7 | 8 | @Singleton 9 | class LoginController @Inject()(cc: ControllerComponents) extends AbstractController(cc) { 10 | def login(name: String, password: String) = Action { 11 | if (check(name, password)) 12 | Redirect("/auth/index") 13 | .withSession(("user", name)) 14 | else BadRequest("Invalid username or password") 15 | } 16 | 17 | def index = Action { implicit request => 18 | request.session.get("user") match { 19 | case Some(user) if isValidUser(user) => Ok(s"Welcome $user") 20 | case Some(user) => BadRequest("Not a valid user") 21 | case None => BadRequest("You are currently not logged in. \nPlease login by calling: \n" + 22 | "http://localhost:9000/auth/login?name=admin&password=1234") 23 | } 24 | } 25 | 26 | def logout = Action { implicit request => 27 | request.session.get("user") match { 28 | case Some(user) if isValidUser(user) => Ok("Successfully logged out").withNewSession 29 | case Some(user) => BadRequest("Not a valid user") 30 | case None => BadRequest("Not logged in. Please login by calling: \n" + 31 | "http://localhost:9000/auth/login?name=admin&password=1234") 32 | } 33 | } 34 | 35 | def check(name: String, password: String): Boolean = { 36 | name == "admin" && password == "1234" 37 | } 38 | 39 | def isValidUser(name: String): Boolean = { 40 | name == "admin" 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /first-app/app/controllers/Users.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | import javax.inject.Singleton 5 | 6 | import play.api.mvc._ 7 | 8 | @Singleton 9 | class Users @Inject()(cc: ControllerComponents) extends AbstractController(cc) { 10 | 11 | def getAllUsers = Action { 12 | Ok.apply("") 13 | } 14 | 15 | def getUser(name: String, age: Int) = Action { 16 | Ok(s"Hello $name of age: $age") 17 | } 18 | 19 | def addUser() = Action { implicit request => 20 | val body = request.body 21 | 22 | body.asFormUrlEncoded match { 23 | case Some(map) => 24 | //persist user information 25 | Ok(s"The user of name `${map("name").head}` and age `${map("age").head}` has been created\n") 26 | case None => BadRequest("Unknow body format") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /first-app/app/json/JsonReadExamples.scala: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json._ 5 | 6 | 7 | object JsonReadExamples { 8 | def main(args: Array[String]): Unit = { 9 | read2 10 | read3 11 | } 12 | 13 | case class Team(teamName: String, players: List[Player], location: Location) 14 | 15 | case class Player(name: String, age: Int) 16 | 17 | case class Location(lat: Double, long: Double) 18 | 19 | val jsonString = 20 | """{ 21 | | "teamName" : "Real Madrid FC", 22 | | "players" : [ { 23 | | "name" : "Ronaldo", 24 | | "age" : 36 25 | | }, { 26 | | "name" : "Modric", 27 | | "age" : 30 28 | | }, { 29 | | "name" : "Bale", 30 | | "age" : 27 31 | | } ], 32 | | "location" : { 33 | | "lat" : 40.4168, 34 | | "long" : 3.7038 35 | | } 36 | |} 37 | |""".stripMargin 38 | 39 | 40 | def read1() = { 41 | val jValue = Json.parse(jsonString) 42 | println((jValue \ "teamName").as[String]) 43 | println((jValue \ "location" \ "lat").as[Double]) 44 | println(((jValue \ "players") (0) \ "name").as[String]) 45 | 46 | val validate: JsResult[String] = (jValue \ "teaName").validate[String] 47 | validate match { 48 | case x:JsSuccess[String] => println(x.get) 49 | case e: JsError => println(e.errors) 50 | } 51 | 52 | val names: Seq[JsValue] = jValue \\ "name" 53 | println(names.map(x => x.as[String])) 54 | } 55 | 56 | /** 57 | * In the below method, we manually generate Reads object for each case class type 58 | */ 59 | def read2() = { 60 | 61 | val jValue: JsValue = Json.parse(jsonString) 62 | 63 | val temp: Reads[Double] = (JsPath \ "location" \ "lat").read[Double] 64 | 65 | println(jValue.as[Double](temp)) 66 | 67 | 68 | implicit val locationReads: Reads[Location] = ( 69 | (JsPath \ "lat").read[Double] and 70 | (JsPath \ "long").read[Double] 71 | ) (Location.apply _) 72 | 73 | implicit val playerReads: Reads[Player] = ( 74 | (JsPath \ "name").read[String] and 75 | (JsPath \ "age").read[Int] 76 | ) (Player.apply _) 77 | 78 | implicit val teamReads: Reads[Team] = ( 79 | (JsPath \ "teamName").read[String] and 80 | (JsPath \ "players").read[List[Player]] and 81 | (JsPath \ "location").read[Location] 82 | ) (Team.apply _) 83 | 84 | 85 | val teams = jValue.as[Team] 86 | 87 | } 88 | 89 | 90 | def read3 = { 91 | val jValue: JsValue = Json.parse(jsonString) 92 | 93 | implicit val playerReads = Json.reads[Player] 94 | implicit val locationReads = Json.reads[Location] 95 | implicit val teamReads = Json.reads[Team] 96 | 97 | val teams = Json.fromJson[Team](jValue).get 98 | 99 | 100 | } 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /first-app/app/json/JsonWriteExamples.scala: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import play.api.libs.functional.syntax._ 4 | import play.api.libs.json._ 5 | 6 | 7 | object JsonWriteExamples { 8 | def main(args: Array[String]): Unit = { 9 | write1 10 | write4 11 | 12 | } 13 | 14 | 15 | val team: Team = Team("Real Madrid FC", List( 16 | Player("Ronaldo", 31), 17 | Player("Modric", 30), 18 | Player("Bale", 27) 19 | ), Location(40.4168, 3.7038)) 20 | 21 | 22 | private def write1 = { 23 | val json: JsValue = JsObject(Seq( 24 | "teamName" -> JsString("Real Madrid FC"), 25 | "players" -> JsArray(Seq( 26 | JsObject(Seq( 27 | "name" -> JsString("Ronaldo"), 28 | "age" -> JsNumber(32))), 29 | JsObject(Seq( 30 | "name" -> JsString("Modric"), 31 | "age" -> JsNumber(29))), 32 | JsObject(Seq( 33 | "name" -> JsString("Bale"), 34 | "age" -> JsNumber(28))) 35 | )), 36 | "location" -> JsObject(Seq( 37 | "lat" -> JsNumber(40.4168), 38 | "long" -> JsNumber(3.7038))) 39 | )) 40 | println(json.toString()) 41 | } 42 | 43 | private def write2 = { 44 | val json: JsValue = Json.obj( 45 | "teamName" -> "Real Madrid FC", 46 | "players" -> Json.arr( 47 | Json.obj( 48 | "name" -> JsString("Ronaldo"), 49 | "age" -> JsNumber(32)), 50 | Json.obj( 51 | "name" -> "Modric", 52 | "age" -> 29), 53 | Json.obj( 54 | "name" -> "Bale", 55 | "age" -> 28) 56 | ), 57 | "location" -> Json.obj( 58 | "lat" -> 40.4168, 59 | "long" -> 3.7038) 60 | ) 61 | println(json.toString()) 62 | } 63 | 64 | private def write3 = { 65 | 66 | Json.obj( 67 | "teamName" -> team.teamName, 68 | "players" -> Json.arr( 69 | team.players.map(x => Json.obj( 70 | "name" -> x.name, 71 | "age" -> x.age 72 | ))), 73 | "location" -> Json.obj( 74 | "lat" -> team.location.lat, 75 | "lat" -> team.location.long 76 | ) 77 | ) 78 | } 79 | 80 | 81 | private def write4 = { 82 | implicit val locationWrites: Writes[Location] = ( 83 | (JsPath \ "lat").write[Double] and 84 | (JsPath \ "long").write[Double] 85 | )(unlift(Location.unapply)) 86 | 87 | implicit val playerWrites: Writes[Player] = ( 88 | (JsPath \ "name").write[String] and 89 | (JsPath \ "age").write[Int] 90 | )(unlift(Player.unapply)) 91 | 92 | implicit val teamWrites: Writes[Team] = ( 93 | (JsPath \ "teamName").write[String] and 94 | (JsPath \ "players").write[List[Player]] and 95 | (JsPath \ "location").write[Location] 96 | )(unlift(Team.unapply)) 97 | 98 | println(Json.toJson(team).toString()) 99 | 100 | } 101 | 102 | def writes5={ 103 | implicit val residentWrites = Json.writes[Player] 104 | implicit val locationWrites = Json.writes[Location] 105 | implicit val positionWrites = Json.writes[Team] 106 | 107 | 108 | println(Json.prettyPrint(Json.toJson(team))) 109 | 110 | } 111 | 112 | case class Team(teamName: String, players: List[Player], location: Location) 113 | 114 | case class Player(name: String, age: Int) 115 | 116 | case class Location(lat: Double, long: Double) 117 | 118 | 119 | } 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /first-app/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(customer:String) 2 | 3 | 4 |

    Welcome @customer

    5 | 6 | -------------------------------------------------------------------------------- /first-app/build.sbt: -------------------------------------------------------------------------------- 1 | name := "first-app" 2 | 3 | version := "1.0.0-SNAPSHOT" 4 | 5 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 6 | 7 | scalaVersion := "2.12.2" 8 | 9 | libraryDependencies += guice -------------------------------------------------------------------------------- /first-app/conf/application.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/first-app/conf/application.conf -------------------------------------------------------------------------------- /first-app/conf/routes: -------------------------------------------------------------------------------- 1 | GET / controllers.HomeController.index 2 | GET /:name controllers.HomeController.hello(name:String) 3 | GET /sqrt/:num controllers.HomeController.sqrt(num:String) 4 | 5 | GET /user/getAllUsers controllers.Users.getAllUsers 6 | GET /user/:name/:age controllers.Users.getUser(name:String, age:Int) 7 | POST /user/addUser controllers.Users.addUser 8 | 9 | GET /auth/index controllers.LoginController.index 10 | GET /auth/logout controllers.LoginController.logout 11 | GET /auth/login controllers.LoginController.login(name:String, password: String) 12 | 13 | 14 | -------------------------------------------------------------------------------- /first-app/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /first-app/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Typesafe repository 2 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/maven-releases/" 3 | 4 | // Use the Play sbt plugin for Play projects 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") 6 | 7 | 8 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") 9 | -------------------------------------------------------------------------------- /search-app/.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 | -------------------------------------------------------------------------------- /search-app/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.ActorSystem 6 | import play.api.mvc._ 7 | 8 | @Singleton 9 | class Application @Inject() (cc: ControllerComponents, akkaSystem: ActorSystem) extends AbstractController(cc) { 10 | 11 | implicit val exec = akkaSystem.dispatchers.lookup("contexts.db-lookups") 12 | 13 | def index = Action { 14 | Ok(views.html.index("Your new application is ready.")) 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /search-app/app/controllers/SearchController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.libs.ws.WSClient 6 | import play.api.mvc._ 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | @Singleton 11 | class SearchController @Inject()(cc: ControllerComponents, 12 | ws: WSClient)(implicit ec: ExecutionContext) 13 | extends AbstractController(cc) { 14 | 15 | 16 | def get(name: String) = Action.async { implicit request => 17 | //below will make a call to 18 | //http://httpbin.org/get?username=ronaldo 19 | ws.url("http://httpbin.org/get") 20 | .addHttpHeaders("Content-Type" -> "application/json") 21 | .withQueryStringParameters("userame" -> name) 22 | .get().map { response => 23 | Ok(response.body) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /search-app/app/service/SearchService.scala: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import java.io.File 4 | 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | trait SearchService { 8 | 9 | def search(word: String, file: File)(implicit exec: ExecutionContext): Future[Boolean] 10 | 11 | def searchInAll(word: String, root: File)(implicit exec: ExecutionContext): Future[Seq[File]] = { 12 | if (!root.isDirectory) { 13 | search(word, root).map(found => if (found) Seq(root) else Seq()) 14 | } else { 15 | val all = root.listFiles().toList.map(x => searchInAll(word, x)) 16 | Future.sequence(all).map(x => x.flatten) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /search-app/app/service/impl/SearchServiceImpl.scala: -------------------------------------------------------------------------------- 1 | package service.impl 2 | 3 | import java.io.File 4 | 5 | import service.SearchService 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | 10 | object SearchServiceImpl extends SearchService{ 11 | override def search(word: String, file: File)(implicit exec: ExecutionContext): Future[Boolean] = Future{ 12 | val content = scala.io.Source.fromFile(file) 13 | content.getLines().exists(_ contains word) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /search-app/app/utils/AppExecutionContext.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | class AppExecutionContext { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /search-app/app/utils/Contexts.scala: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import akka.actor.ActorSystem 6 | import play.api.libs.concurrent.Akka 7 | 8 | @Singleton 9 | class Contexts @Inject() (akkaSystem: ActorSystem) { 10 | implicit val dbLookup = akkaSystem.dispatchers.lookup("contexts.db-lookups") 11 | implicit val expensiveDBLookup = akkaSystem.dispatchers.lookup("contexts.expensive-db-lookups") 12 | implicit val cpuLookup = akkaSystem.dispatchers.lookup("contexts.cpu-operations") 13 | } 14 | -------------------------------------------------------------------------------- /search-app/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | @message 6 | 7 | } 8 | -------------------------------------------------------------------------------- /search-app/build.sbt: -------------------------------------------------------------------------------- 1 | name := "SearchApp" 2 | 3 | version := "1.0" 4 | 5 | lazy val `searchapp` = (project in file(".")).enablePlugins(PlayScala) 6 | 7 | scalaVersion := "2.11.7" 8 | 9 | libraryDependencies ++= Seq( jdbc , ws , guice , specs2 % Test ) 10 | 11 | resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" 12 | 13 | libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.1" 14 | -------------------------------------------------------------------------------- /search-app/conf/application.conf: -------------------------------------------------------------------------------- 1 | logger.root=ERROR 2 | 3 | # Logger used by the framework: 4 | logger.play=INFO 5 | 6 | # Logger provided to your application: 7 | logger.application=DEBUG 8 | 9 | akka { 10 | actor { 11 | default-dispatcher { 12 | fork-join-executor { 13 | # Settings this to 1 instead of 3 seems to improve performance. 14 | parallelism-factor = 1.0 15 | 16 | # @richdougherty: Not sure why this is set below the Akka 17 | # default. 18 | parallelism-max = 24 19 | 20 | # Setting this to LIFO changes the fork-join-executor 21 | # to use a stack discipline for task scheduling. This usually 22 | # improves throughput at the cost of possibly increasing 23 | # latency and risking task starvation (which should be rare). 24 | task-peeking-mode = LIFO 25 | } 26 | } 27 | } 28 | } 29 | 30 | contexts { 31 | db-lookups{ 32 | throughput = 1 33 | thread-pool-executor { 34 | fixed-pool-size = 10 35 | } 36 | } 37 | expensive-db-lookups { 38 | executor = "thread-pool-executor" 39 | throughput = 1 40 | thread-pool-executor { 41 | fixed-pool-size = 20 42 | } 43 | } 44 | cpu-operations { 45 | fork-join-executor { 46 | parallelism-max = 2 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /search-app/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 /get/:name controllers.SearchController.get(name) 8 | -------------------------------------------------------------------------------- /search-app/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 -------------------------------------------------------------------------------- /search-app/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 4 | 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") 6 | -------------------------------------------------------------------------------- /search-app/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/search-app/public/images/favicon.png -------------------------------------------------------------------------------- /search-app/public/stylesheets/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scala-microservices-book/book-examples/7edf3d6d7df4c6c5032c6e8e800187c45c774c41/search-app/public/stylesheets/main.css -------------------------------------------------------------------------------- /search-app/test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | 9 | @RunWith(classOf[JUnitRunner]) 10 | class IntegrationSpec extends Specification { 11 | 12 | "Application" should { 13 | 14 | "work from within a browser" in new WithBrowser { 15 | 16 | browser.goTo("http://localhost:" + port) 17 | 18 | browser.pageSource must contain("Your new application is ready.") 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /search-app/test/service/impl/SearchSpec.scala: -------------------------------------------------------------------------------- 1 | package service.impl 2 | 3 | import java.io._ 4 | 5 | import org.junit.runner.RunWith 6 | import org.specs2.mutable._ 7 | import org.specs2.runner._ 8 | 9 | import scala.concurrent.duration._ 10 | import scala.concurrent.{Await, Future} 11 | import scala.util.Random 12 | import scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | @RunWith(classOf[JUnitRunner]) 15 | object SearchSpec extends Specification { 16 | 17 | "SearchService" should { 18 | "search word in file" in { 19 | val file = prepareFile("hello.txt", "hi") 20 | 21 | val ans: Future[Boolean] = SearchServiceImpl.search("hi", file) 22 | 23 | Await.result(ans, 10 seconds) must equalTo(true) 24 | } 25 | 26 | "search in all nested files" in { 27 | val root = new File("root") 28 | root.mkdir() 29 | val hi0 = prepareFile("hello0.txt", "hi0", root) 30 | prepareFile("hello1.txt", "hi1", root) 31 | prepareFile("hello2.txt", "hi2", root) 32 | 33 | val folder = new File(root, "test") 34 | folder.mkdir() 35 | val hi1 = prepareFile("hello3.txt", "hi0",folder) 36 | val hi2 = prepareFile("hello4.txt", "hi0",folder) 37 | 38 | Await.result(SearchServiceImpl.searchInAll("hi0", root), 1000 seconds).toSet must equalTo(Set(hi0, hi1, hi2)) 39 | 40 | } 41 | 42 | } 43 | 44 | private def prepareFile(fileName: String, wordToContain: String, parentFolder:File = new File(".")) = { 45 | val f = new File(parentFolder, fileName) 46 | f.deleteOnExit() 47 | 48 | val writer = new BufferedWriter(new FileWriter(f)) 49 | for (i <- 1 to 10) { 50 | writer.write(Random.nextString(i)) 51 | writer.write("\n") 52 | } 53 | writer.write(wordToContain) 54 | writer.close() 55 | f 56 | } 57 | 58 | } 59 | 60 | --------------------------------------------------------------------------------