├── .bowerrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── activator.properties ├── app ├── Global.scala ├── backend │ └── PostRepo.scala └── controllers │ └── Posts.scala ├── bower.json ├── build.sbt ├── conf ├── application.conf ├── play.plugins └── routes ├── package.json ├── project ├── build.properties └── plugins.sbt ├── public ├── app │ ├── index.html │ ├── post-card.html │ ├── post-elements.html │ └── post-list.html ├── images │ ├── JAVEO_200x140.png │ ├── avatar-01.svg │ ├── avatar-02.svg │ ├── avatar-03.svg │ ├── avatar-04.svg │ ├── avatar-05.svg │ ├── avatar-06.svg │ ├── avatar-07.svg │ ├── avatar-08.svg │ ├── avatar-09.svg │ ├── avatar-10.svg │ ├── avatar-11.svg │ ├── avatar-12.svg │ ├── avatar-13.svg │ ├── avatar-14.svg │ ├── avatar-15.svg │ ├── avatar-16.svg │ └── favicon.png ├── post-service │ └── post-service.html └── stylesheets │ └── main.css ├── test └── controllers │ └── PostsSpec.scala └── tutorial └── index.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/components" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .sbtserver/ 17 | project/.sbtserver 18 | project/.sbtserver.lock 19 | project/play-fork-run.sbt 20 | project/sbt-ui.sbt 21 | projectFilesBackup/ 22 | 23 | # Bower plugin 24 | /node_modules/ 25 | /public/components/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.7 4 | jdk: 5 | - oraclejdk8 6 | 7 | script: sbt ++$TRAVIS_SCALA_VERSION test bower 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Activator Template by Typesafe 2 | 3 | Licensed under Public Domain (CC0) 4 | 5 | To the extent possible under law, the person who associated CC0 with 6 | this Activator Tempate has waived all copyright and related or neighboring 7 | rights to this Activator Template. 8 | 9 | You should have received a copy of the CC0 legalcode along with this 10 | work. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Play Framework - ReactiveMongo - Polymer [![Build Status](https://travis-ci.org/JAVEO/play-reactivemongo-polymer.svg?branch=master)](https://travis-ci.org/JAVEO/play-reactivemongo-polymer) 2 | 3 | The purpose of this application is to demonstrate a Service Oriented Approach for Single Page App development. 4 | 5 | To serve this purpose this application template integrates **Polymer**, **Play Framework** and **MongoDB**. 6 | 7 | For sake of this template this [sample app] (https://www.polymer-project.org/docs/start/tutorial/intro.html) available 8 | on Polymer website has been used. 9 | 10 | ## Getting started 11 | 12 | Download frontend dependencies (requires installed node.js): 13 | 14 | sbt bower 15 | 16 | and run app 17 | 18 | activator run 19 | 20 | -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=play-reactivemongo-polymer 2 | title=Play Framework with REST, ReactiveMongo and Polymer 3 | description=A starter application with Play Framework, ReactiveMongo and Polymer employing REST services. 4 | tags=playframework,scala,mongo,reactivemongo,polymer 5 | authorName=JAVEO 6 | authorLink=http://javeo.eu 7 | authorLogo=https://raw.githubusercontent.com/JAVEO/play-reactivemongo-polymer/master/public/images/JAVEO_200x140.png 8 | authorBio=JAVEO is a software house specialized in custom development for the JVM platform. We are the professionals making next generation, scalable and responsive solutions for the enterprises, B2B startups and the mobile. We are a globally available team - just a few pings away, within your instant reach. Be open, be connected and leverage this opportunity. 9 | authorTwitter=javeo_eu 10 | -------------------------------------------------------------------------------- /app/Global.scala: -------------------------------------------------------------------------------- 1 | import javax.inject.Inject 2 | 3 | import play.api.Play.current 4 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 5 | import play.api.libs.iteratee.Enumerator 6 | import play.api.libs.json.Json 7 | import play.api.{ Logger, Application, GlobalSettings } 8 | 9 | import play.modules.reactivemongo.ReactiveMongoApi 10 | import play.modules.reactivemongo.json.collection.JSONCollection 11 | 12 | class Global @Inject() ( 13 | val reactiveMongoApi: ReactiveMongoApi) extends GlobalSettings { 14 | 15 | def collection = reactiveMongoApi.db.collection[JSONCollection]("posts") 16 | 17 | val posts = List( 18 | Json.obj( 19 | "text" -> "Have you heard about the Web Components revolution?", 20 | "username" -> "Eric", 21 | "avatar" -> "../images/avatar-01.svg", 22 | "favorite" -> false 23 | ), 24 | Json.obj( 25 | "text" -> "Loving this Polymer thing.", 26 | "username" -> "Rob", 27 | "avatar" -> "../images/avatar-02.svg", 28 | "favorite" -> false 29 | ), 30 | Json.obj( 31 | "text" -> "So last year...", 32 | "username" -> "Dimitri", 33 | "avatar" -> "../images/avatar-03.svg", 34 | "favorite" -> false 35 | ), 36 | Json.obj( 37 | "text" -> "Pretty sure I came up with that first.", 38 | "username" -> "Ada", 39 | "avatar" -> "../images/avatar-07.svg", 40 | "favorite" -> false 41 | ), 42 | Json.obj( 43 | "text" -> "Yo, I heard you like components, so I put a component in your component.", 44 | "username" -> "Grace", 45 | "avatar" -> "../images/avatar-08.svg", 46 | "favorite" -> false 47 | ), 48 | Json.obj( 49 | "text" -> "Centralize, centrailize.", 50 | "username" -> "John", 51 | "avatar" -> "../images/avatar-04.svg", 52 | "favorite" -> false 53 | ), 54 | Json.obj( 55 | "text" -> "Has anyone seen my cat?", 56 | "username" -> "Zelda", 57 | "avatar" -> "../images/avatar-06.svg", 58 | "favorite" -> false 59 | ), 60 | Json.obj( 61 | "text" -> "Decentralize!", 62 | "username" -> "Norbert", 63 | "avatar" -> "../images/avatar-05.svg", 64 | "favorite" -> false 65 | ) 66 | ) 67 | 68 | override def onStart(app: Application) { 69 | Logger.info("Application has started") 70 | 71 | collection.bulkInsert(posts.toStream, ordered = true). 72 | foreach(i => Logger.info("Database was initialized")) 73 | } 74 | 75 | override def onStop(app: Application) { 76 | Logger.info("Application shutdown...") 77 | 78 | collection.drop().onComplete { 79 | case _ => Logger.info("Database collection dropped") 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/backend/PostRepo.scala: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import play.api.libs.json.{JsObject, Json} 4 | import play.modules.reactivemongo.ReactiveMongoApi 5 | import play.modules.reactivemongo.json.collection.JSONCollection 6 | import reactivemongo.api.ReadPreference 7 | import reactivemongo.api.commands.WriteResult 8 | import reactivemongo.bson.{BSONObjectID, BSONDocument} 9 | 10 | import scala.concurrent.{ExecutionContext, Future} 11 | 12 | trait PostRepo { 13 | def find()(implicit ec: ExecutionContext): Future[List[JsObject]] 14 | 15 | def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] 16 | 17 | def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] 18 | 19 | def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] 20 | } 21 | 22 | class PostMongoRepo(reactiveMongoApi: ReactiveMongoApi) extends PostRepo { 23 | // BSON-JSON conversions 24 | import play.modules.reactivemongo.json._ 25 | 26 | protected def collection = 27 | reactiveMongoApi.db.collection[JSONCollection]("posts") 28 | 29 | def find()(implicit ec: ExecutionContext): Future[List[JsObject]] = 30 | collection.find(Json.obj()).cursor[JsObject](ReadPreference.Primary).collect[List]() 31 | 32 | def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = collection.update(selector, update) 33 | 34 | def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = collection.remove(document) 35 | 36 | def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = 37 | collection.update(BSONDocument("_id" -> document.get("_id").getOrElse(BSONObjectID.generate)), document, upsert = true) 38 | } 39 | -------------------------------------------------------------------------------- /app/controllers/Posts.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Inject 4 | 5 | import play.api.libs.concurrent.Execution.Implicits.defaultContext 6 | import play.api.libs.json.Json 7 | import play.api.mvc.{ Action, BodyParsers, Call, Controller, Result } 8 | 9 | import reactivemongo.bson.{ BSONObjectID, BSONDocument } 10 | import reactivemongo.core.actors.Exceptions.PrimaryUnavailableException 11 | import reactivemongo.api.commands.WriteResult 12 | 13 | import play.modules.reactivemongo.{ 14 | MongoController, ReactiveMongoApi, ReactiveMongoComponents 15 | } 16 | 17 | class Posts @Inject() (val reactiveMongoApi: ReactiveMongoApi) 18 | extends Controller with MongoController with ReactiveMongoComponents { 19 | 20 | import controllers.PostFields._ 21 | 22 | def postRepo = new backend.PostMongoRepo(reactiveMongoApi) 23 | 24 | def list = Action.async {implicit request => 25 | postRepo.find() 26 | .map(posts => Ok(Json.toJson(posts.reverse))) 27 | .recover {case PrimaryUnavailableException => InternalServerError("Please install MongoDB")} 28 | } 29 | 30 | def like(id: String) = Action.async(BodyParsers.parse.json) { implicit request => 31 | val value = (request.body \ Favorite).as[Boolean] 32 | postRepo.update(BSONDocument(Id -> BSONObjectID(id)), BSONDocument("$set" -> BSONDocument(Favorite -> value))) 33 | .map(le => Ok(Json.obj("success" -> le.ok))) 34 | } 35 | 36 | def update(id: String) = Action.async(BodyParsers.parse.json) { implicit request => 37 | val value = (request.body \ Text).as[String] 38 | postRepo.update(BSONDocument(Id -> BSONObjectID(id)), BSONDocument("$set" -> BSONDocument(Text -> value))) 39 | .map(le => Ok(Json.obj("success" -> le.ok))) 40 | } 41 | 42 | def delete(id: String) = Action.async { 43 | postRepo.remove(BSONDocument(Id -> BSONObjectID(id))) 44 | .map(le => RedirectAfterPost(le, routes.Posts.list())) 45 | } 46 | 47 | private def RedirectAfterPost(result: WriteResult, call: Call): Result = 48 | if (result.inError) InternalServerError(result.toString) 49 | else Redirect(call) 50 | 51 | def add = Action.async(BodyParsers.parse.json) { implicit request => 52 | val username = (request.body \ Username).as[String] 53 | val text = (request.body \ Text).as[String] 54 | val avatar = (request.body \ Avatar).as[String] 55 | postRepo.save(BSONDocument( 56 | Text -> text, 57 | Username -> username, 58 | Avatar -> avatar, 59 | Favorite -> false 60 | )).map(le => Redirect(routes.Posts.list())) 61 | } 62 | } 63 | 64 | object PostFields { 65 | val Id = "_id" 66 | val Text = "text" 67 | val Username = "username" 68 | val Avatar = "avatar" 69 | val Favorite = "favorite" 70 | } 71 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "play-reactivemongo-polymer", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "private": true, 6 | "ignore": [ 7 | "**/.*", 8 | "node_modules", 9 | "bower_components", 10 | "components", 11 | "test", 12 | "tests" 13 | ], 14 | "dependencies": { 15 | "polymer": "Polymer/polymer#^1.2.0", 16 | "gold-phone-input": "PolymerElements/gold-phone-input#^1.0.0", 17 | "iron-ajax": "PolymerElements/iron-ajax#^1.0.0", 18 | "iron-icon": "PolymerElements/iron-icon#^1.0.0", 19 | "iron-icons": "PolymerElements/iron-icons#^1.0.0", 20 | "iron-validator-behavior": "PolymerElements/iron-validator-behavior#^1.0.0", 21 | "paper-button": "PolymerElements/paper-button#^1.0.0", 22 | "paper-dialog": "PolymerElements/paper-dialog#^1.0.0", 23 | "paper-fab": "PolymerElements/paper-fab#^1.0.0", 24 | "paper-icon-button": "PolymerElements/paper-icon-button#^1.0.0", 25 | "paper-input": "PolymerElements/paper-input#^1.0.0", 26 | "paper-spinner": "PolymerElements/paper-spinner#^1.0.0", 27 | "paper-tabs": "PolymerElements/paper-tabs#^1.0.0", 28 | "paper-toast": "PolymerElements/paper-toast#^1.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := """PlayReactiveMongoPolymer""" 2 | 3 | version := "1.0-SNAPSHOT" 4 | 5 | scalaVersion := "2.11.7" 6 | 7 | routesGenerator := InjectedRoutesGenerator 8 | 9 | libraryDependencies ++= Seq( 10 | "org.reactivemongo" %% "play2-reactivemongo" % "0.11.7.play24", 11 | "org.specs2" %% "specs2-core" % "3.6.5" % "test", 12 | "org.specs2" %% "specs2-junit" % "3.6.5" % "test", 13 | "org.specs2" %% "specs2-mock" % "3.6.5" % "test" 14 | ) 15 | 16 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 17 | 18 | JsEngineKeys.engineType := JsEngineKeys.EngineType.Node 19 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | application.secret="1f5UEufe^l6iVYii8Lm7g:H4[C<9uaPbsqKwtHE/t`NWF@XRv=cC]KmYe^8jvO`9" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | application.langs="en" 16 | 17 | # Global object class 18 | # ~~~~~ 19 | # Define the Global object class for this application. 20 | # Default to Global in the root package. 21 | # application.global=Global 22 | 23 | # Router 24 | # ~~~~~ 25 | # Define the Router object to use for this application. 26 | # This router will be looked up first when the application is starting up, 27 | # so make sure this is the entry point. 28 | # Furthermore, it's assumed your route file is named properly. 29 | # So for an application router like `my.application.Router`, 30 | # you may need to define a router file `conf/my.application.routes`. 31 | # Default to Routes in the root package (and conf/routes) 32 | # application.router=my.application.Routes 33 | 34 | # Database configuration 35 | # ~~~~~ 36 | # You can declare as many datasources as you want. 37 | # By convention, the default datasource is named `default` 38 | # 39 | # db.default.driver=org.h2.Driver 40 | # db.default.url="jdbc:h2:mem:play" 41 | # db.default.user=sa 42 | # db.default.password="" 43 | 44 | # Evolutions 45 | # ~~~~~ 46 | # You can disable evolutions if needed 47 | # evolutionplugin=disabled 48 | 49 | 50 | play.modules.enabled += "play.modules.reactivemongo.ReactiveMongoModule" 51 | 52 | mongodb.uri = "mongodb://localhost:27017/posts" 53 | -------------------------------------------------------------------------------- /conf/play.plugins: -------------------------------------------------------------------------------- 1 | 1100:play.modules.reactivemongo.ReactiveMongoPlugin -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Assets.at(path="/public", file="app/index.html") 7 | 8 | GET /api/posts controllers.Posts.list 9 | PATCH /api/post/:id/like controllers.Posts.like(id: String) 10 | PATCH /api/post/:id controllers.Posts.update(id: String) 11 | POST /api/post controllers.Posts.add 12 | DELETE /api/post/:id controllers.Posts.delete(id : String) 13 | 14 | GET /*file controllers.Assets.at(path="/public", file) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "bower": "~1.6" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Tue Mar 03 13:10:16 CET 2015 3 | template.uuid=88679796-ef1c-40cd-82c2-d3114cfa2881 4 | sbt.version=0.13.9 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" 2 | 3 | // The Play plugin 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.3") 5 | addSbtPlugin("com.github.dwickern" % "sbt-bower" % "1.0.3") 6 | -------------------------------------------------------------------------------- /public/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Posts 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | All 54 | Favorites 55 | 56 | 57 |
58 | 59 |
60 | 61 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /public/app/post-card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 66 | 100 | 101 | 157 | -------------------------------------------------------------------------------- /public/app/post-elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 32 | 39 | 40 | 41 | 78 | 79 | 80 | 81 | 82 | 93 | 105 | 106 | 107 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /public/app/post-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 | 57 | 58 | 135 | -------------------------------------------------------------------------------- /public/images/JAVEO_200x140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVEO/play-reactivemongo-polymer/ef9fb543b33763e2efa7738a06cf25308ca3c37b/public/images/JAVEO_200x140.png -------------------------------------------------------------------------------- /public/images/avatar-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 19 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/images/avatar-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/images/avatar-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 51 | 69 | 70 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /public/images/avatar-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 25 | 27 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/images/avatar-05.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/images/avatar-06.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 13 | 17 | 31 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/images/avatar-07.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 13 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/images/avatar-08.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 15 | 16 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/images/avatar-09.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 57 | 71 | 72 | 74 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /public/images/avatar-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 19 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/images/avatar-11.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 30 | 31 | 36 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/images/avatar-12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/images/avatar-13.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 28 | 30 | 31 | 32 | 33 | 39 | 45 | 46 | 47 | 48 | 51 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/images/avatar-14.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 26 | 27 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/images/avatar-15.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 18 | 20 | 21 | 27 | 28 | 29 | 34 | 35 | 37 | 38 | 41 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/images/avatar-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 13 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVEO/play-reactivemongo-polymer/ef9fb543b33763e2efa7738a06cf25308ca3c37b/public/images/favicon.png -------------------------------------------------------------------------------- /public/post-service/post-service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 41 | 42 | 93 | -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVEO/play-reactivemongo-polymer/ef9fb543b33763e2efa7738a06cf25308ca3c37b/public/stylesheets/main.css -------------------------------------------------------------------------------- /test/controllers/PostsSpec.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import backend.PostMongoRepo 4 | import controllers.PostFields.{Avatar, Favorite, Id, Text, Username} 5 | import org.specs2.matcher.{Expectable, Matcher} 6 | import org.specs2.mock.Mockito 7 | import play.api.libs.json._ 8 | import play.api.mvc._ 9 | import play.api.test.Helpers._ 10 | import play.api.test._ 11 | import play.modules.reactivemongo.ReactiveMongoApi 12 | import reactivemongo.api.commands.LastError 13 | import reactivemongo.bson.BSONDocument 14 | 15 | import scala.concurrent.{ExecutionContext, Future} 16 | import scala.concurrent.ExecutionContext.Implicits.global 17 | 18 | object PostsSpec extends org.specs2.mutable.Specification with Results with Mockito { 19 | 20 | val mockPostRepo = mock[PostMongoRepo] 21 | val reactiveMongoRepo = mock[ReactiveMongoApi] 22 | 23 | val FirstPostId = "5559e224cdecd3b535e8b681" 24 | val SecondPostId = "5559e224cdecd3b535e8b682" 25 | 26 | val wojciechPost = Json.obj( 27 | Id -> FirstPostId, 28 | Text -> "Have you heard about the Javeo?", 29 | Username -> "Wojciech", 30 | Avatar -> "../images/avatar-01.svg", 31 | Favorite -> true 32 | ) 33 | val arunPost = Json.obj( 34 | Id -> SecondPostId, 35 | Text -> "Microservices: the good, the bad, and the ugly", 36 | Username -> "Arun", 37 | Avatar -> "../images/avatar-03.svg", 38 | Favorite -> false 39 | ) 40 | val controller = new TestController() 41 | val nothingHappenedLastError = new LastError(true, None, None, None, 0, None, false, None, None, false, None, None) 42 | 43 | case class PostBSONDocumentMatcher(expected: BSONDocument) extends Matcher[BSONDocument] { 44 | override def apply[S <: BSONDocument](t: Expectable[S]) = { 45 | result(evaluate(t), 46 | t.description + " is valid", 47 | t.description + " is not valid", 48 | t) 49 | } 50 | 51 | def evaluate[S <: BSONDocument](t: Expectable[S]): Boolean = { 52 | t.value.get(Text) === expected.get(Text) && 53 | t.value.get(Username) === expected.get(Username) && 54 | t.value.get(Avatar) === expected.get(Avatar) 55 | } 56 | } 57 | 58 | class TestController() extends Posts(reactiveMongoRepo) { 59 | override def postRepo: PostMongoRepo = mockPostRepo 60 | } 61 | 62 | "Posts Page#list" should { 63 | "list posts" in { 64 | mockPostRepo.find()(any[ExecutionContext]) returns Future(List(wojciechPost, arunPost)) 65 | 66 | val result: Future[Result] = controller.list().apply(FakeRequest()) 67 | 68 | contentAsJson(result) must be equalTo JsArray(List(arunPost, wojciechPost)) 69 | } 70 | } 71 | 72 | "Posts Page#delete" should { 73 | "remove post" in { 74 | mockPostRepo.remove(any[BSONDocument])(any[ExecutionContext]) returns Future(nothingHappenedLastError) 75 | 76 | val result: Future[Result] = controller.delete(FirstPostId).apply(FakeRequest()) 77 | 78 | status(result) must be equalTo SEE_OTHER 79 | redirectLocation(result) must beSome(routes.Posts.list().url) 80 | there was one(mockPostRepo).remove(any[BSONDocument])(any[ExecutionContext]) 81 | } 82 | } 83 | 84 | 85 | "Posts Page#add" should { 86 | "create post" in { 87 | mockPostRepo.save(any[BSONDocument])(any[ExecutionContext]) returns Future(nothingHappenedLastError) 88 | val post = Json.obj( 89 | Text -> "Loving basketball", 90 | Username -> "Martin", 91 | Avatar -> "avatar.svg" 92 | ) 93 | 94 | val request = FakeRequest().withBody(post) 95 | val result: Future[Result] = controller.add()(request) 96 | 97 | status(result) must be equalTo SEE_OTHER 98 | redirectLocation(result) must beSome(routes.Posts.list().url) 99 | 100 | val document = BSONDocument(Text -> "Loving basketball", Username -> "Martin", Avatar -> "avatar.svg") 101 | there was one(mockPostRepo).save(argThat(PostBSONDocumentMatcher(document)))(any[ExecutionContext]) 102 | } 103 | } 104 | 105 | "Posts Page#like" should { 106 | "like post" in { 107 | mockPostRepo.update(any[BSONDocument], any[BSONDocument])(any[ExecutionContext]) returns Future(nothingHappenedLastError) 108 | 109 | val request = FakeRequest().withBody(Json.obj("favorite" -> true)) 110 | val result: Future[Result] = controller.like(SecondPostId)(request) 111 | 112 | status(result) must be equalTo OK 113 | contentAsJson(result) must be equalTo Json.obj("success" -> true) 114 | there was one(mockPostRepo).update(any[BSONDocument], any[BSONDocument])(any[ExecutionContext]) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Play Framework - ReactiveMongo - Polymer 6 | 7 | 8 |
9 |

Play Framework - ReactiveMongo - Polymer

10 |

11 | The purpose of this application is to demonstrate a Service Oriented Approach for Single Page App development. 12 |

13 |

14 | To serve this purpose this application template integrates Polymer, Play Framework and MongoDB. 15 |

16 | 17 |
18 | 19 |
20 |

Database

21 |

22 | A document based NoSQL MongoDB database is used along with ReactiveMongo driver to connect to it in asynchronous, non-blocking way. 23 |

24 | 25 |

26 | To install MongoDB on your computer you can use this manual. 27 |

28 | 29 |

30 | To connect to database Play-ReactiveMongo plugin is used. 31 | To make it work the following has been done: 32 |

33 |
    34 |
  1. Adding dependency in build.sbt 35 |
    "org.reactivemongo" %% "play2-reactivemongo" % "0.11.7.play24"
    36 |
  2. 37 |
  3. Adding plugin in play.plugins 38 |
    1100:play.modules.reactivemongo.ReactiveMongoPlugin
    39 |
  4. 40 |
  5. Adding database url in application.conf 41 |
    mongodb.uri = "mongodb://localhost:27017/posts"
    42 |
  6. 43 |
44 | 45 |
46 | 47 |
48 |

REST API

49 | 50 |

51 | The application tier is delivered by a simple Play app. 52 | This way you can easily take advantage of Play's ecosystem, eg. add some plugins or other 53 | benefits, such as Akka, CoffeScript support Web Sockets and many, many more. 54 | The template has one Controller: Posts. 55 | It has PostRepo which uses Play-ReactiveMongo plugin under the hood. 56 | 57 | 58 |

59 |

60 | PostRepo provides db object to access specific database collections: 61 |

 62 |                 
 63 |                     protected def collection = db.collection[JSONCollection]("posts")
 64 |                 
 65 |             
66 |

67 | 68 |

69 | To get data from the database use list method. It's use looks like this: 70 |

 71 |                 
 72 |                     def list = Action.async {implicit request =>
 73 |                         postRepo.find()
 74 |                         .map(posts => Ok(Json.toJson(posts.reverse)))
 75 |                         .recover {case PrimaryUnavailableException => InternalServerError("Please install MongoDB")}
 76 |                     }
 77 |                 
 78 |             
79 | In this case the response is json with all posts from the database collection: 80 |
 81 |                 
 82 |                     [
 83 |                         {
 84 |                         "_id": {"$oid":"556848238600001401dfead6"},
 85 |                         "text" : "Have you heard about the Web Components revolution?",
 86 |                         "username" : "Eric",
 87 |                         "avatar" : "../images/avatar-01.svg",
 88 |                         "favorite": false
 89 |                         },
 90 |                         {
 91 |                         "uid": {"$oid":"556848238600001401dfead7"},
 92 |                         "text" : "Loving this Polymer thing.",
 93 |                         "username" : "Rob",
 94 |                         "avatar" : "../images/avatar-02.svg",
 95 |                         "favorite": false
 96 |                         },
 97 |                         ...
 98 |                     ]
 99 |                 
100 |             
101 |

102 | 103 |

104 | To like post : In the template it accepts json body with favorite, boolean flag. The method saves this flag in the database collection: 105 |

106 |                 
107 |                     def like(id: String) = Action.async(BodyParsers.parse.json) { implicit request =>
108 |                         val value = (request.body \ Favorite).as[Boolean]
109 |                         postRepo.update(BSONDocument(Id -> BSONObjectID(id)), BSONDocument("$set" -> BSONDocument(Favorite -> value)))
110 |                         .map(le => Ok(Json.obj("success" -> le.ok)))
111 |                     }
112 |                 
113 |             
114 |

115 | 116 |

117 | To update post's message : In the template it accepts json body with text, String parameter: 118 |

119 |                 
120 |                     def update(id: String) = Action.async(BodyParsers.parse.json) { implicit request =>
121 |                         val value = (request.body \ Text).as[String]
122 |                         postRepo.update(BSONDocument(Id -> BSONObjectID(id)), BSONDocument("$set" -> BSONDocument(Text -> value)))
123 |                         .map(le => Ok(Json.obj("success" -> le.ok)))
124 |                     }
125 |                 
126 |             
127 |

128 | 129 |

130 | To delete post : 131 |

132 |                 
133 |                     def delete(id: String) = Action.async {
134 |                         postRepo.remove(BSONDocument(Id -> BSONObjectID(id)))
135 |                         .map(le => RedirectAfterPost(le, routes.Posts.list()))
136 |                     }
137 |                 
138 |             
139 |

140 | 141 |

142 | To add post : 143 |

144 |                 
145 |                     def add = Action.async(BodyParsers.parse.json) { implicit request =>
146 |                         val username = (request.body \ Username).as[String]
147 |                         val text = (request.body \ Text).as[String]
148 |                         val avatar = (request.body \ Avatar).as[String]
149 |                         postRepo.save(BSONDocument(
150 |                             Text -> text,
151 |                             Username -> username,
152 |                             Avatar -> avatar,
153 |                             Favorite -> false
154 |                         )).map(le => Redirect(routes.Posts.list()))
155 |                     }
156 |                 
157 |             
158 |

159 | 160 |

161 | To expose REST API in Play framework the routes file has been configured: 162 |

163 |                 
164 |                     GET     /api/posts                  controllers.Posts.list
165 |                     PATCH   /api/post/:id/like          controllers.Posts.like(id: String)
166 |                     PATCH   /api/post/:id               controllers.Posts.update(id: String)
167 |                     POST    /api/post                   controllers.Posts.add
168 |                     DELETE  /api/post/:id               controllers.Posts.delete(id : String)
169 |                 
170 |             
171 |

172 |
173 | 174 |
175 |

Frontend

176 |

177 | Frontend is based on Polymer which uses 178 | Web Components. 179 | It's a very prospecting technology, promising true component development of the web. 180 |

181 |

182 | For sake of this template this 183 | sample app available on Polymer 184 | website has been used. 185 |

186 |

187 | All resources from the sample app have been copied to public directory in Play application. 188 | (We are looking forward to publishing polymer and web components as webjars in the future to use them as dependencies instead). 189 | Application is built from the post-list element which represents a list of posts. 190 | A single post is represented by the post-card element. 191 |

192 |

193 | Polymer elements encapsulate template for view and script for behavior: 194 |

195 |                 
196 |             <dom-module id="unique-id">
197 |                 <template>
198 |                     ...
199 |                 </template>
200 |             </dom-module>
201 | 
202 |             <script>
203 |                 Polymer({
204 |                     is: 'unique-id',
205 |                     ...
206 |                 });
207 |             </script>
208 |                 
209 |             
210 |

211 |

212 | Use data binding in your markup to fill your template with data: 213 |

214 |                 
215 |     <img src="[[item.avatar]]" width="70" height="70">
216 |     <h2>[[item.username]]<h2>
217 |                 
218 |             
219 |

220 |
221 | 222 |
223 |

Communication

224 |

225 | The core-ajax component in post-service element is used to call backend REST services from Polymer: 226 |

227 |                 
228 |     <iron-ajax id="ajax"
229 |                url="/api/posts"
230 |                handle-as="json"
231 |                last-response="{{posts}}">
232 |     </iron-ajax>
233 |                 
234 |             
235 |

236 | 237 |

238 | The posts element attribute is public and can be used by other elements: 239 |

240 |                 
241 |     <post-service id="service" posts="{{posts}}" ...></post-service>
242 |                 
243 |             
244 |

245 | 246 |

247 | To set favorite flag for a post you have to call the like REST URL. 248 | Specify the HTTP method, url, body and contentType: 249 |

250 |                 
251 |     <iron-ajax id="likeCall"
252 |                method="PATCH"
253 |                content-type="application/json"
254 |                handle-as="json"
255 |                on-response="refresh">
256 |     </iron-ajax>
257 |                 
258 |             
259 |

260 |

261 | To make the call bind your parameters and invoke go method on the like-call iron-ajax component: 262 |

263 |                 
264 |     setFavorite: function(id, isFavorite) {
265 |         this.$.likeCall.url = '/api/post/' + id + '/like';
266 |         this.$.likeCall.body = JSON.stringify({ "favorite": isFavorite });
267 |         this.$.likeCall.generateRequest();
268 |     }
269 |                 
270 |             
271 |

272 |
273 |
274 |

Summary

275 |

276 | MongoDB and Play Framework are a great mix to implement backend web services, easy to integrate with contemporary UI frameworks. 277 |

278 |

279 | Thanks to Mongo you can store data without worrying about the schema and Play can expose this data very easily via REST API. 280 |

281 |

282 | It is a very powerful technology stack with the UI in cutting edge Polymer library and Web components. 283 |

284 |
285 | 286 | 287 | --------------------------------------------------------------------------------