In this tutorial we will implement the Ribbit application in Scala. We'll be covering how to install the Play web framework, a NetBeans plugin for it, and finally the code in Scala. If you are new to Scala, check out this previous tutorial which will help you set up your environment and provides you with a general platform that you can build upon.
Even though the essence of Ribbit is to create/send/read Ribbits (our version of tweets), we will spend a large part of this tutorial explaining how Play works, authentication, and persistence. After these are in place, the rest becomes much easier. We will also implement ribbit creation, submission and listing out all ribbits. Following someone, advanced user settings, and direct messages will be an extra assignment for you to complete on your own. I am sure if you manage to follow along with this tutorial and create Ribbit as explained below, these three functionalities will be easily accomplished as homework.
Download and Install Play
There are quite a few web frameworks for Scala. Some are purely functional and a few are somewhat MVC-ish. I've chosen Play for this example because it resembles an MVC architecture and it should be more familiar to people used to these kinds of frameworks. Additionally, it is one of the best and most recommended web frameworks for Scala.
So, go ahead and download Play for Scala. Please note this tutorial was written when the latest stable version was 2.1.1, you may need to adapt the content below for newer versions.
Installing Play is as simple as extracting the archive into a folder with both read and write permissions and then adding the path to the "play" executable to your PATH
. At the time of writing this article, the only requirement for Play is Java 6.
Installing the Play Plugin for NetBeans
There is a very nice plugin for NetBeans which will make your life easier while working with Play. It allows you to create and manage Play projects directly from NetBeans. Go ahead and download the Play plugin for NetBeans from the official plugin page. After you have the .nbm
file downloaded, just add it to NetBeans using its plugin manager.
Next, you'll need to specify the path to the "play" executable for your NetBeans plugin. Go to Tools / Options / Miscellaneous / Play. Browse to where you extracted Play's archive, until you can find the executable itself. Here's what it looked like for me:
Creating the Project
In NetBeans select File / New Project and choose Play / Simple Scala Application.
Then specify the folder you want your project to reside in. NetBeans will generate basic settings and a directory structure for you.
Configuration for Building the Project
Before you can run your project, you have to make sure the correct SBT version is being used. By default version 0.11.3 is required, but I have 0.12.2 installed. You may have another version. So switch to the "Files" view in NetBeans and locate the Play project you created. If you are not using NetBeans just use your favorite file manager to find the project's folder. You will find there, in the "project" subfolder a file called "build.properties"
. Edit it and change the sbt.version
to the correct value. If you don't have SBT installed, please review the tutorial I mentioned in the introduction to this article. It explains all the details on how to install SBT.
If you have recently updated SBT, don't forget to republish the NetBeans SBT plugin locally. Go to your "nbsbt" folder and run these commands:
rm -r ~/.ivy2/local/org.netbeans.nbsbt sbt clean compile publish-local
This will also solve any errors telling you that SBT can't find the NetBeans plugin or that an incompatible binary type was detected.
However, Play has a little bug, and can not use the user's ~/.ivy2
repository. Instead, it uses its own local repositories. There are several workarounds that you can find on the Internet, the easiest one I've found, is to just create a symbolic link in Play's folder / repository/local
that points to ~/ivy2/local/org.netbeans.nbsbt
.
In the same directory, in the plugins.sbt
file, make sure the proper Play version is set at this line:
// Use the Play sbt plugin for Play projects addSbtPlugin("play" % "sbt-plugin" % "2.1.1")
Finally, in the same directory, in the file called Build.scala
, make sure the application's name has no white spaces. If you specified your project with white spaces in its name, this will be wrongly set.
val appName = "RibbitInScala"
Afterwards, you should be able to right click on the Ribbit in Scala project in the "Projects" view and successfully execute "Build". Building the project for the first time will take a little while to complete. You should see the progress in an SBT window in NetBeans. Subsequent builds should take only a few seconds.
Finally, we can run our project. Just right click on it and select "Run". In NetBeans' output window, you will see instructions on how to access your project. If all goes well, these should be the last lines in that log:
[info] Done updating. --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000 (Server started, use Ctrl+D to stop and go back to the console...)
We can now see our application running on localhost's port 9000.
This is the default Play welcome screen. It is like this because we have not yet implemented any actual application code. If you see this, you are up and running and ready for the next section. Congratulations.
Basic Play Configuration
Play is very similar to other MVC web frameworks, so I won't go into great detail about its inner workings. In this section, let's just create a simple "Hello World" view and controller, which will be shown using a default route, so that we can get accustomed with Play's architecture.
In the Configuration/routes
file, you can see that every call to the root (/) is redirected to the Application controller.
GET / controllers.Application.index
In controllers, you will find Application.scala
. The code in this file will render the "index"
template from views.
def index = Action { Ok(views.html.index("Your new application is ready.")) }
In the views
folder, there are a couple of files: index.scala.html
and main.scala.html
. The first one will call the second one to show you the content. In the second one, there is an HTML structure, CSS inclusions, JavaScript library inclusions and a few other things. Feel free to play around with these files, but explaining the content in them is however not the concern of this article.
In Application.scala
change the parameter being passed into index()
to "Hello World"
.
Ok(views.html.index("Hello World"))
Then modify index.scala.html to display it.
@(message: String) @main("Our first content") { <p>@message</p> }
The first line in a template file is the function signature. (message: String)
means this view is getting a single parameter called "message"
of the type String
. Then comes @main
, a function that returns a paragraph HTML tag with the message contained inside of it. But for all of this to work we need main.scala.html
also. This has a different signature. The first parameter which is the title (check your browser's tab / title) and the other one is the content. "Our first content"
will be the title and the return value of main will be the content.
@(title: String)(content: Html)
title
is a String and content
must be HTML. The rest of the main.scala.html
file is self-explanatory. After a refresh in your browser, you will see "Hello World" instead of the previous documentation and welcome screen.
If you want to learn more about Play's inner workings check out the official documentation.
Creating the Home Page and Login Screen
As usual with these Ribbit tutorials, we will work with the following resources introduced in "The Design" article. So, go ahead and download the layout.zip file and extract it. We will refer to this folder as "LAYOUT_HOME"
.
Getting the Files
We need less.js in our Play project, so copy it from LAYOUT_HOME
into our project's "public/javascripts"
folder. Then copy style.less
from LAYOUT_HOME
to our project's "public/Stylesheets"
folder. Continue with copying LAYOUT_HOME/gfx/frog.jpg
, logo-nettuts.png
and logo.png
to our project's "public/images"
folder.
Preparing the Views
At this point we don't care about authentication and form generation. We will just take and adapt the HTML code from LAYOUT_HOME/home.html
so that we have something visual to work with.
The HTML skeleton of our Ribbit app will reside in the view called main.scala.html
. You actually already have this view generated, as we've seen it in the previous section, now just modify it like so:
@(title: String)(content: Html) <!DOCTYPE html> <html> <head> <title>@title</title> <link rel="stylesheet" media="screen" href='@routes.Assets.at("stylesheets/main.css")'> <link rel="stylesheet/less" media="screen" href='@routes.Assets.at("stylesheets/style.less")'> <link rel="shortcut icon" type="image/png" href='@routes.Assets.at("images/favicon.png")'> <script src='@routes.Assets.at("javascripts/jquery-1.7.1.min.js")' type="text/javascript"></script> <script src='@routes.Assets.at("javascripts/less.js")' type="text/javascript"></script> </head> <body> <header> <div class="wrapper"> <img src='@routes.Assets.at("images/logo.png")'> <span>Twitter Clone</span> <form> <input name="email" type="text"> <input name="password" type="password"> </form> </div> </header> @content <footer> <div class="wrapper"> Ribbit - A Twitter Clone Tutorial<img src='@routes.Assets.at("images/logo-nettuts.png")'> </div> </footer> </body> </html>
As you can see the code now references the "style.less"
file we introduced and the "less.js"
JavaScript and all paths to images were also updated to use Play's '@routes.Assets.at(...)'
syntax. To avoid confusing the editors with double quotes inside double quoted HTML tags, I've chosen to single quote the HTML tags that are using Play variables or methods.
The next file we need to update is our index.scala.html
view. This is the index file providing the content for the main page. What we return from this will be inserted into main.scala.html
at the line where "@content"
is specified.
@(message: String) @main("Ribbit in Scala & Play") { <div id="content"> <div class="wrapper"> <img src='@routes.Assets.at("images/frog.jpg")'> <div class="panel right"> <h1>New to Ribbit?</h1> <p> <form> <input name="email" type="text"> <input name="password" type="text"> <input name="password2" type="password"> <input type="submit" value="Create Account"> </form> </div> </div> </div> }
In this case we just changed the title that is passed to main
and returned plain old HTML code.
Now, if you run your project and access http://localhost:9000 you should see an almost working home page for Ribbit.
It's not yet perfect, but we have styled forms and all images are correctly referenced.
Correcting the Stylesheets
Now we have to check and update our style.less
file to use the backgrounds and other things from our image
directory. Of course, the first thing will be to copy the rest of the files from LAYOUT_HOME/gfx
to our project's "public/images"
folder.
Play with Scala, supports LESS right out of the box. To use this feature we first need to drop less.js
from our views and transform our style.less
stylesheet into a Play asset. Start by just deleting the following line from main.scala.html
:
<script src='@routes.Assets.at("javascripts/less.js")' type="text/javascript"></script>
Now, if you refresh your project's page in the browser, it will look ugly and unstyled.
Then move "style.less"
from the project's "public/Stylesheets"
folder into the "app/assets/stylesheets"
folder. Create it, if needed. Rename "style.less"
into "main.less"
. Play will automatically compile it into "public/Stylesheets/main.css"
and we can delete the reference to "style.less"
from main.scala.html
.
Refresh your browser window again and make sure nothing is cached. You may need to stop and run your Play project again for the effects to take place. If all goes well, you should see a page similar to how it looked like initially. There may be some slight differences, but don't worry, we will fix them in a minute.
Here is how the "head"
part of your main.scala.html
file should look like now.
<head> <title>@title</title> <link rel="stylesheet" media="screen" href='@routes.Assets.at("stylesheets/main.css")'> <link rel="shortcut icon" type="image/png" href='@routes.Assets.at("images/favicon.png")'> <script src='@routes.Assets.at("javascripts/jquery-1.7.1.min.js")' type="text/javascript"></script> </head>
Finally, edit "main.less"
and replace all "gfx/"
folder specifications for images with "/assets/images/"
. You may need to fix a couple widths and heights also, because the less file may be compiled differently than the JavaScript version. Afterwards, here is what we end up with:
User Management
Play's Built-in Authentication Functions
For our example, we will simply use Play's built-in authentication mechanisms. These are pretty good and quite complex, so we will only take what is absolutely needed for us. In Application.scala
we will define a form and some actions for authentication.
package controllers import play.api._ import play.api.mvc._ import play.api.data._ import play.api.data.Forms._ import models._ import views._ object Application extends Controller { val loginForm = Form( tuple( "email" -> text, "password" -> text ) verifying ("Invalid email or password", result => result match { case (email, password) => check(email, password) }) ) def check(username: String, password: String) = { (username == "john.doe@gmail.com" && password == "123") } def index = Action { implicit request => Ok(html.index(loginForm)) } def authenticate = Action { implicit request => loginForm.bindFromRequest.fold( formWithErrors => BadRequest(html.index(formWithErrors)), user => Redirect(routes.Application.public).withSession("email" -> user._1) ) } def logout = Action { Redirect(routes.Application.index).withNewSession.flashing( "success" -> "You've been logged out" ) } def public = Action { implicit request => Ok(html.public("Logged in")("John Doe")) } }
loginForm
is a value we will use to authenticate our user. This form will be used in the index.scala.html
view and it will call the authenticate action on our controller (we will study the view in a moment). The validation on the form will kick in and the pattern match will force a call to the "check"
method. For the time being, check
only returns a true or false value for the hard-coded user, john.doe@gmail.com and password 123.
The index
function renders the index
view with the loginForm sent in as parameter. We need this to be shown on our home page.
The authenticate
function binds to the form: if there are any errors, it goes back to the index page or if it was successful, it will render a view called "public"
with the session for that view, populated with the user's email. This is a very important step because if you omit the ".withSession"
call, even though your user is authenticated, you will have no way of knowing which user was logged in.
Finally, logout
clears up the session while the public
function renders the public
view, at this point with just the hard-coded user name "John Doe".
Update Routes
In the previous section we created several new actions for our controller. We now have to update our routes to accommodate these new actions:
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ # Home page GET / controllers.Application.index # Authentication POST /login controllers.Application.authenticate GET /logout controllers.Application.logout # Message GET /public controllers.Application.public # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file)
We added definitions for login, logout, public and of course for the authenticate action called from the form. This is enough for the time being.
Add the Login Form to the Header
We now have a controller and route actions, next we'll update our views. The first one to take care of is main.scala.html
. We will have to update the "header" part so that it will display a form, from the controller, instead of the two built-in inputs.
<header> <div class="wrapper"> <img src='@routes.Assets.at("images/logo.png")'> <span>Twitter Clone</span> @loginForm </div> </header>
@loginForm
has to be specified as a parameter, so the first line of the file becomes:
@(title: String)(loginForm: Html)(content: Html)
Now we will build the form's content in the index.scala.html
view and pass it to main
.
@(form: Form[(String,String)])(implicit flash: Flash) @main("Welcome to Ribbit in Scala") { @helper.form(routes.Application.authenticate) { <input type="email" name="email" placeholder="Email" id="email" value='@form("email").value'> <input type="password" name="password" id="password" placeholder="Password"> } } { <div id="content"> <div class="wrapper"> <img src='@routes.Assets.at("images/frog.jpg")'> <div class="panel right"> <h1>New to Ribbit?</h1> <p> <form> <input name="email" type="text"> <input name="password" type="text"> <input name="password2" type="password"> <input type="submit" value="Create Account"> </form> </div> </div> </div> }
One important piece of code in this view is: @helper.form(routes.Application.authenticate)
. This is how we call authenticate
on our Application controller from the form.
Create a Public View
Finally, create the public.scala.html
view so we have a destination to redirect to in case of a successful login. This will be mostly empty at this point.
@(title: String)(user: String) @main("Public Tweets") { <a id='btnLogOut' href="@routes.Application.logout" class="logout">logout</a> } { <div>@user</div> }
Getting a Slick Database Backend
Scala database backends are very cool, because they provide a syntax which hides SQL or other data languages and they map their functionality directly as Scala classes. You may think of them like ORM libraries, but implemented in a much more optimal way. Our database backend of choice is called Slick. Installing it will be as simple as adding it as a dependency in our build.scala
.
"com.typesafe.slick" %% "slick" % "1.0.1", "com.h2database" % "h2" % "1.3.166"
The first line is the Slick library, while H2 is the database that we will use. H2 is the equivalent of MySQL or Postgres or whatever your favorite database server may be. Of course, these lines go into the appDependencies
definition.
val appDependencies = Seq( "org.mindrot" % "jbcrypt" % "0.3m", "com.typesafe.slick" %% "slick" % "1.0.1", "com.h2database" % "h2" % "1.3.166" )
Accounts, Databases and Authentication
Managing Accounts With a Model
It's now time to make our application actually do some kind of authentication. We will introduce a model called Account. Create a file called Account.scala
in your source packages / models
folder. You may have to create the models
folder if it's missing.
The Account model will be responsible for user management. It will connect to the database and find our John Doe user, authenticate him, and provide other useful search methods for us. Here's what our model looks like:
package models import org.mindrot.jbcrypt.BCrypt import scala.slick.driver.H2Driver.simple._ import Database.threadLocalSession case class Account(id: Int, email: String, password: String, name: String) object Account { object Users extends Table[(Int, String, String, String)]("USERS"){ def id = column[Int]("ID", O.PrimaryKey) def email = column[String]("EMAIL") def password = column[String]("PASSWORD") def name = column[String]("NAME") def * = id ~ email ~ password ~ name } def authenticate(email: String, password: String): Option[Account] = { findByEmail(email).filter { account => BCrypt.checkpw(password, account.password) } } def findByEmail(email: String): Option[Account] = findBy("email", email) def findById(id: Int): Option[Account] = findBy("id", id.toString) def findAll(): Seq[Account] = findById(1).toSeq def findBy(field: String, value: String): Option[Account] = { val user = Database.forURL("jdbc:h2:mem:users", driver = "org.h2.Driver") withSession { Users.ddl.create Users.insert(1, "John.Doe@gmail.com", BCrypt.hashpw("123", BCrypt.gensalt()), "John Doe") val foundUsers = field match { case "email" => for { u <- Users if u.email.toLowerCase === value.toLowerCase } yield(u) case "id" => for { u <- Users if u.id === value.toInt } yield(u) } foundUsers.firstOption } user map { case (id, email, password, name) => new Account(id, email, password, name) } } } class NullAccount(id: Int = -1, email: String = "not@set", password: String = "", name: String = "Unknown") extends Account(id: Int, email: String, password: String, name: String) { }
Well, it's pretty long, allow me to explain it line-by-line.
- Line 1 – the package for this file has to be "models"
- Lines 3-5 – import encryption, database driver and session libraries
- Lines 7-8 – we define a class Account and an object for it with constructor parameters
- Lines 11-15 – we define a User object that extends Table. This is a link between Scala and our database. In it, we have all the parameters that characterize a user.
- Lines 20-22 – define a function called
"authenticate"
. This will be called from the Application controller's form instead of "check". It will return an optional Account object. It may return None if the user is not found or the authentication fails. The logic in this class calls another method defined below which returns an Account for an email and then checks if the account's password matches what we typed in the form. At this point we also introduced encryption for the password. - Lines 24-26 – two functions for finding a user by email and by id. We may not need the
findById
but I've put it in here for exemplification. - Line 28 – will find all of the users. At this point though, it is not yet implemented, we just return a user with id 1 as a sequence.
- Lines 30-49 – is where most of our logic is contained.
findBy
is a method that connects to an H2 database residing in memory. This is a temporary DB and we will just insert John Doe into it. File persistence will come a little later. Next, thefindBy
method finds all of the users from the DB, based on ID or email, depending on the parameter specified. Finally, it returns anOption[Account]
. - Lines 53-55 – we define a null version of Account. Just in case we end up on the public page with an unknown, but authenticated user, we don't want our view to break.
Connecting the Application Controller to Accounts
Now that Account can authenticate our user, we can get rid of the "check"
function in the Application controller and change our form into this:
val loginForm = Form( tuple( "email" -> text, "password" -> text ) verifying ("Invalid email or password", result => result match { case (email, password) => Account.authenticate(email, password).isDefined }) )
Instead of calling check
we will call Account.authenticate
and check if the returned value is defined, as in the case of a failure, it can return None
.
def public = Action { implicit request => def getLoggedInEmail = request.session.get("email") match { case Some(email) => email case None => "" } def getUserFromOption(user: Option[Account]) = user match { case Some(account) => account case None => new NullAccount } val user = getUserFromOption(Account.findByEmail(getLoggedInEmail)) Ok(html.public("Logged in")(user)) }
The public function has to be updated as well to get a user account by the email address, kept in the session.
Update the View
The public.scala.html
view has to be updated in order to get a parameter of type Account and be able to obtain all user information from it.
@(title: String)(user: Account) @main("Public Tweets") { <a id='btnLogOut' href="@routes.Application.logout" class="logout">logout</a> } { <div>@user.name</div> }
Here, we just output user's name at this point.
Creating Users and Using Real Persistence for Them
Now that we have authentication with our database working, it's time to make the database persistent. It is also a good time to implement user creation. We will again be forced to rethink parts of our actual design. In previous sections we started our reasoning and development with the business logic (the code in Applications.scala
and in Account.scala
) and worked our way up to the views. I propose we take a reverse approach this time, so that you can see an example where we start with index.scala.html
, our view, and develop step by step toward the functionality that we want, ending with Account.scala
.
Writing a Play Form for Account Creation
We have to modify index.scala.html
so that our second form, for the user creation, will also be created using Play's helpers, so that this code:
<p> <form> <input name="email" type="text"> <input name="password" type="text"> <input name="password2" type="password"> <input type="submit" value="Create Account"> </form>
Becomes this, instead:
@helper.form(routes.Application.createAccount) { <input type="text" name="name" placeholder="Full Name" id="name" value='@createForm("name").value'> <input type="email" name="email" placeholder="Email" id="email" value='@createForm("email").value'> <input type="password" name="password" id="password" placeholder="Password"> <input type="password" name="confirm" id="password2" placeholder="Confirm Password"> <input type="submit" value="Create Account"> }
In this code, there are two essential pieces:
-
@createForm("...").value
– which is a form that we will send in from the controller. This also requires us to change the signature of our view (see below). -
routes.Application.createAccount
– which is the action that we will create in our controller. This requires us to update our routes and of course the Application controller.
The view's signature becomes:
@(loginForm: Form[(String,String)], createForm: Form[(String, String, String, String)])(implicit flash: Flash)
As you can see, we are now always getting two forms. To better differentiate them, we also updated the name of the previous variable to "loginForm"
, so we have to update the input for login as well.
<input type="email" name="email" placeholder="Email" id="email" value='@loginForm("email").value'>
We also had to update main.less
to accommodate the extra input, but these small marginal adjustments here-and-there are not that relevant.
Adding the New Route
This is easy, just one more line in the "routes"
file.
# User Creation POST /create controllers.Application.createAccount
Adding in New Controller Functionality
First, add a new form for creating a user:
val createForm = Form( tuple( "name" -> text, "email" -> text, "password" -> text, "confirm" -> text ) verifying ("Invalid email or password", result => result match { case (name, email, password, confirm) => Account.create(name, email, password, confirm).isDefined }) )
This is where we define the four fields that we'll need. Then in the case where we call Account.create(...).isDefined
, this will force us to write a "create"
function in our Account model.
Next, we need to define the "createAccount"
action:
def createAccount = Action { implicit request => createForm.bindFromRequest.fold( formWithErrors => BadRequest(html.index(loginForm,formWithErrors)), user => Redirect(routes.Application.public).withSession("email" -> user._2) ) }
For this action, on failure, we will redirect to the index page and send in the loginForm
plus our formWithErrors
. This will help keep the user's filled in form data present, after a form submission. On success, we will just go to the "public" page, as we do when the authentication succeeds. We will also set and pass the email through the session.
Finally, we need to adjust all the other calls to the index
view to take both forms as parameters:
def index = Action { implicit request => Ok(html.index(loginForm, createForm)) } def authenticate = Action { implicit request => loginForm.bindFromRequest.fold( formWithErrors => BadRequest(html.index(formWithErrors, createForm)), user => Redirect(routes.Application.public).withSession("email" -> user._1) ) }
Creating the Account in the Database
Up until now, all of our code for user creation was just to support the framework: views, routes, controller actions, forms. Now it's time for some serious business. We will need to change Account.scala
quite a bit, so below is the code for the changed version and then afterwards, the explanation.
package models import org.mindrot.jbcrypt.BCrypt import scala.slick.driver.H2Driver.simple._ import Database.threadLocalSession case class Account(email: String, password: String, name: String) object Account { object User extends Table[(String, String, String)]("USERS"){ def email = column[String]("EMAIL", O.PrimaryKey) def password = column[String]("PASSWORD") def name = column[String]("NAME") def * = email ~ password ~ name } def authenticate(email: String, password: String): Option[Account] = { findByEmail(email).filter { account => BCrypt.checkpw(password, account.password) } } def create(name: String, email: String, password: String, confirm: String): Option[Account] = { if (password != confirm) None else { Database.forURL("jdbc:h2:users", driver = "org.h2.Driver") withSession { try { User.ddl.create } catch { case e: org.h2.jdbc.JdbcSQLException => println("Skipping table createion. It already exists.") } User.insert(email, BCrypt.hashpw(password, BCrypt.gensalt()), name) } findByEmail(email) } } def findByEmail(email: String): Option[Account] = findBy("email", email) def findByName(name: String): Option[Account] = findBy("name", name) def findAll(): Seq[Account] = findByName("John Doe").toSeq def findBy(field: String, value: String): Option[Account] = { val user = Database.forURL("jdbc:h2:users", driver = "org.h2.Driver") withSession { val foundUser = field match { case "email" => for { u <- User if u.email.toLowerCase === value.toLowerCase } yield(u) case "name" => for { u <- User if u.name.toLowerCase === value.toLowerCase } yield(u) } foundUser.firstOption } user map { case (email, password, name) => new Account(email, password, name) } } } class NullAccount(email: String = "not@set", password: String = "", name: String = "Unknown") extends Account(email: String, password: String, name: String) { }
Here's what we've done:
- We dropped the
"id"
for the user, as theemail
address is just as good as anid
and we can use it for the unique key. This also led to changes in the Account's signature, in NullAccount, and in the User's signature. Users was renamed to User and now has only three fields: email, password, and name. - Because we removed the
"id"
, we also changed the function"findBy"
. Now it can search by name and email and not by id. - To implement real persistence, we dropped the "mem" specification from the database connection. This makes "USERS" a real table residing in a database file called
"users"
. - Finally, we added a create method to add the new user to the database. In its logic, it tries to create the table and in case it already exists, it catches the exception and just writes a nice message to the console. Check your NetBeans' Output console, the one that opens when you select "Run" on the project. The rest of the code should be fairly obvious, insert the new user into the database and then look it up and return it with the already existing function
"findByEmail"
.
We are done. You can now create users. Have fun with them!
Post a Ribbit
Now that we are more familiar with Play, Slick and Scala, the rest of the program is quite easy to build. We will, however, implement both posting ribbits and showing all ribbits. This is also the final state of the attached source code, so I will only mention the most interesting parts here.
To create Ribbits we have to modify our public view. As a first step, you can take the code from the layout ribbit tutorial we used at the beginning and start changing it where needed. First, we need a form generated by Play instead of the hardcoded one.
<div class="panel right"> <h1>Create a Ribbit</h1> <p> @helper.form(routes.Ribbits.createRibbit) { <textarea type="text" name="ribbit" placeholder="Your Ribbit here..." id="ribbit" class="ribbitText" value='@createForm("name").value' ></textarea> <input type="submit" value="Ribbit!"> } </p> </div>
Don't forget to update the view's signature as well.
Then we create a new controller, called Ribbits. We can now move the "public"
function here, update our Application controller and our views to call Ribbits.public
instead of Application.public
. We then add a "createRibbit"
function in the new controller, together with our form.
package controllers [...] // many omitted imports object Ribbits extends Controller { def createForm (session: Session) = Form( single("ribbit" -> text) verifying ("Could not add Ribbit. Sorry.", result => result match { case (ribbit) => RibbitRepository.create(ribbit, session.get("email"))._1.equals(ribbit) }) ) def getLoggedInUser(session: Session): Account = { def getLoggedInEmail = session.get("email") match { case Some(email) => email case None => "" } def getUserFromOption(user: Option[Account]): Account = user match { case Some(account) => account case None => new NullAccount } getUserFromOption(Account.findByEmail(getLoggedInEmail)) } def public = Action { implicit request => val user = getLoggedInUser(request.session) Ok(html.public("Logged in")(user)(createForm(request.session))(RibbitRepository.findAll)) } def createRibbit = Action { implicit request => createForm(request.session).bindFromRequest.fold( formWithErrors => BadRequest(html.public("Logged in")(getLoggedInUser(request.session))(formWithErrors)(RibbitRepository.findAll)), ribbit => Redirect(routes.Ribbits.public) ) } }
Then we create a new model, RibbitRepository. It will be responsible for creating and listing out all ribbits.
def create(content: String, sender: Option[String]): (String,String,String,String) = { Database.forURL("jdbc:h2:ribbits", driver = "org.h2.Driver") withSession { try { Ribbit.ddl.create } catch { case e: org.h2.jdbc.JdbcSQLException => println("Skipping table createion. It already exists.") } def senderEmail = sender match { case Some(email) => email case None => "Unknown@email.address" } Ribbit.insert(content, senderEmail, new SimpleDateFormat("yyyy-MM-dd HH:mm").format(Calendar.getInstance.getTime)) } findAll().last }
We also modified our database, I just called it "ribbits"
and both the "Ribbits"
and the "Users"
are in this database. A ribbit will have some content, a sender's email and a timestamp.
View All Ribbits
Finally, we updated our view to loop over all ribbits provided by the findAll
function of the RibbitRepository model.
def findAll(): Seq[(String,String,String,String)] = { val allRibbits = Database.forURL("jdbc:h2:ribbits", driver = "org.h2.Driver") withSession { try { Ribbit.ddl.create } catch { case e: org.h2.jdbc.JdbcSQLException => println("Skipping table createion. It already exists.") } val foundRibbits = for { r <- Ribbit u <- User if u.email.toLowerCase === r.sender.toLowerCase } yield((r.content, r.sender, r.dateTime, u.name)) foundRibbits.list } allRibbits }
This same method is also called on ribbit creation, so that the list is refreshed after a submission. The view simply does a map on the sequence and outputs the content.
<div id="ribbits" class="panel left"> <h1>Public Ribbits</h1> @ribbits.map { ribbit => <div class="ribbitWrapper"> <img class="avatar" src='@routes.Assets.at("images/user1.png")'> <span class="name">@ribbit._4</span> @ribbit._2 <span class="time">@ribbit._3</span> <p> @ribbit._1 </p> </div> } </div>
And here's what the final version of the signature for the view should look like:
@(title: String)(user: Account)(createForm: Form[String])(ribbits: Seq[(String,String,String,String)])
Notice that we did not use "user"
in the view, but we could have, so I decided to leave it in there just in case you wish to modify this code and use it however you please.
And here is the finished app in action.
Final Thoughts
So I think now is a good time to end this tutorial. Creating additional pages for the app would be very similar to the ones we have already done here, so I'll leave this as an exercise for you. I hope I helped you to understand the basics of Scala, Play and Slick. Consider this tutorial a very basic introduction, without all of the fancy stuff like Ajax requests or auto incrementing on H2 key columns. I am sure that if you were able to follow along with this tutorial, it will provide you with a solid base that you can build on top of. Also, don't forget to check out all of the extra information and documentation about Slick, Play and Scala that I linked to throughout this tutorial.
Thank you for reading.
No hay comentarios:
Publicar un comentario