├── .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 [](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 |
60 |
61 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/public/app/post-card.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
66 |
67 |
68 |
73 |
74 |
75 |
79 |
80 |
81 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | [[text]]
98 |
99 |
100 |
101 |
157 |
--------------------------------------------------------------------------------
/public/app/post-elements.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 | Rob
36 |
37 |
38 |
39 |
40 |
41 |
78 |
79 |
80 |
81 |
82 |
93 |
94 |
95 | Confirm
96 |
97 | Are you sure to delete this post from {{post.username}} ?
98 |
99 |
103 |
104 |
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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
40 |
41 | [[item.username]]
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
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 |
9 |
13 |
14 |
19 |
20 |
25 |
26 |
32 |
33 |
39 |
40 |
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 | Adding dependency in build.sbt
35 | "org.reactivemongo" %% "play2-reactivemongo" % "0.11.7.play24"
36 |
37 | Adding plugin in play.plugins
38 | 1100:play.modules.reactivemongo.ReactiveMongoPlugin
39 |
40 | Adding database url in application.conf
41 | mongodb.uri = "mongodb://localhost:27017/posts"
42 |
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 |
--------------------------------------------------------------------------------