MVC Application using Java and Play Framework (Contd)
This article is the extension of the previous article.
In this article, I will be enhancing the application developed in the previous article, by:
- Refactoring the Views using Bootstrap and jQuery, so that the Views are presented in a better way.
- Connecting to the Database to persist the data, using Ebean ORM.
Continuing from where we left in the previous article, let us go ahead and Refactor the Views.
Refactoring the Views with Bootstrap
Let us do some refactoring of the Views/HTML pages by adding some styling and moving the common lines of code into other files.
For styling, I will be using Bootstrap.
Download Bootstrap, Font Awesome Icons, jQuery and popper.js
Download the latest version of the Bootstrap from here.
Copy the bootstrap.min.css
file into the /public/css
folder.
Copy the bootstrap.min.js
file into the /public/js
folder.
We will be using the Font Awesome Icons from https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css, directly into our Views Templates.
Download the latest version of the jQuery from here and place the jQuery.min.js
file in the /public/js
folder.
Either use the popper.js
directly from the CDN location https://unpkg.com/@popperjs/core@2 or download and place the file in /public/js
folder.
Create a common layout template
- There are some common(repeated) piece of codes in the Views. We can move them to the common Layout Template.
1<html>
2<head>
3 <title>Todo Tracker Application</title>
4</head>
5<body>
6 <h2 align="center">Todo Tracker Application</h2>
7 ...
8 <script>...</script>
9</body>
10</html>
- Create a template -
views.layout.scala.html
1@(title: String)(content: Html)
2<html lang="en">
3 <head>
4 <title>Todo Tracker Application</title>
5 <!-- Add the bootstrap css from public/css folder -->
6 <link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("css/bootstrap.min.css")">
7 <!-- Add the font-awesome css from the cdn link directly -->
8 <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
9 </head>
10 <body>
11 <!-- Start of navigation menu -->
12 <div class="container">
13 <nav class="navbar navbar-expand-lg navbar-light bg-light">
14 <div class="container-fluid">
15 <a href="@routes.TasksController.getAllTasks" class="navbar-brand">
16 <img src="@routes.Assets.versioned("images/favicon.png")" alt="" width="30" height="24" class="d-inline-block align-text-top">
17 <b>Todo Tracker Application</b></a>
18 @* Here's where we render the page title `String`. *@
19 <h4>@title</h4>
20 <ul class="nav nav-pills">
21 <li class="nav-item"><a href="@routes.TasksController.getAllTasks" class="nav-link" id="home" aria-current="page">Home</a></li>
22 <li class="nav-item"><a href="@routes.TasksController.getCreateForm" id="create" class="nav-link">Create Task</a></li>
23 </ul>
24 </div>
25 </nav>
26 <!-- End of navigation menu -->
27 <br/>
28 @* And here's where we render the `Html` object containing the page content. *@
29 @content
30 </div>
31
32 <!-- It is always advisable to add the script tags inside body tag toward the end for faster rendering of the page -->
33 <!-- Add the required js from public/js folder -->
34 <!-- Add the popper.js and jquery.js before the bootstrap.js -->
35 <script src="@routes.Assets.versioned("js/popper.min.js")"></script>
36 <script src="@routes.Assets.versioned("js/jquery-3.6.0.min.js")"></script>
37 <script src="@routes.Assets.versioned("js/bootstrap.min.js")"></script>
38 <!-- Add all the custom js in custom.js -->
39 <script src="@routes.Assets.versioned("js/custom.js")"></script>
40 </body>
41</html>
- Add the
bootstrap.min.css
andfont-awesome.min.css
files path to thehref
attribute of thelink
tags inside thehead
tag. - Add the
jquery.min.js
,popper.min.js
andbootstrap.min.js
files path to thesrc
attribute of thescript
tags at the bottom of thebody
tag. - Place the
popper.min.js
andjquery.min.js
before thebootstrap.min.js
, asbootstrap.min.js
requires jQuery for loading its contents. - It is always advisable to add the
script
tags insidebody
tag toward the end for faster rendering of the page. - We will add the navigation menu, using the standard bootstrap navigation navbar styling.
- For the src attribute for script tags and href attribute for link tags, we add the path using
@routes.Assets.versioned
in Play framework. This would render the files that are in the/public
folder, since we have the route mapping enabled like given below:
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
Call common layout template from views
We shall update the existing views in the views.tasks
folder to make a call to the layout template.
- We will be removing the head section and also the closing body section in the file and replacing them with the call to layout template. It takes 2 parameters, title and html content.
- Since we have created the Navigation Menu with Create Task tab, we will be removing the Create link from the Homepage.
- We will be updating all the buttons with the relevant Font Awesome Icons.
- We will move all the custom javascript methods to the
custom.js
and place it in the/public/js
folder. - Each view files would have changes like below (highlighted lines).
view.tasks.home.scala.html
1@(tasksList : List[Tasks])
2
3@layout("List All Tasks") {
4 <table class="table table-striped table-hover">
5 <caption style="caption-side: top;font-style:italic;text-align:center;">Click on the row(s) to view the details</caption>
6 <thead align="center">
7 <tr>
8 <th>Title</th>
9 <th>Description</th>
10 <th>Due Date</th>
11 <th>Status</th>
12 <th>No. Of Comments</th>
13 <th>Edit</th>
14 <th>Delete</th>
15 </tr>
16 </thead>
17 <tbody align="center">
18 @for(task <- tasksList) {
19 <tr onclick="rowClick('@routes.TasksController.view(task.getSystemTaskId())');">
20 <td>@task.getTitle()</td>
21 <td>@task.getDescription()</td>
22 <td>@task.getDueDateStr()</td>
23 <td>@task.getStatus()</td>
24 <td align="center">@task.getTodoTaskCommentsSet().size()</td>
25 <td><a class="btn btn-warning" href="@routes.TasksController.edit(task.getSystemTaskId())">
26 <i class="fa fa-edit" aria-hidden="true"></i></a></td>
27 <td><a class="btn btn-danger" href="@routes.TasksController.deleteTask(task.getSystemTaskId())">
28 <i class="fa fa-trash" aria-hidden="true"></i></a></td>
29 </tr>
30 }
31 </tbody>
32 </table>
33}
Add custom.js
We will move all the custom javascript methods to the custom.js
and place it in the /public/js
folder.
1// "Add New Comments" button toggle for adding new comment
2function addNewComments(){
3 var lTable = document.getElementById("commentsTable");
4 lTable.style.display = (lTable.style.display == "table") ? "none" : "table";
5}
6
7//To enable the click on the row functionality, in the Homepage
8function rowClick(iUrl){
9 document.location.href = iUrl;
10}
11
12//To enable the active nav section in the menu
13$(function(){
14 $('a').each(function(){
15 if ($(this).prop('href') == window.location.href) {
16 $(this).addClass('active'); $(this).parents('li').addClass('active');
17 }
18 });
19});
Refactored Views
Home Page
Now when we Stop, Compile and Run the Application Server, we will see the below refractored views.
Create Task Page
Click on the Create Task tab on the navigation menu, at the top.
Fill in the details and click on “Create Task” button, to create new task.
View Task Page
Click on the row to View the details.
Edit/Update Task Page
For Editing the task details, there are 2 options:
- From the Homepage, click on Edit icon.
- From View page, click on Edit icon.
Delete Task
For Deleting the task, there are 2 options:
- From the Homepage, click on Delete icon.
- From View page, click on Delete icon.
Connecting the Database
Till now, we have seen the complete MVC Functionality of Play, Refactoring Views using Bootstrap without even persisting the data. Now, let us look at persisting the data in the database.
The Play Framework has a built-in H2 Database, along with support for JPA with Hibernate and other persistence frameworks.
Enable Play Ebean plugin
Play comes with the Ebean ORM for Persisting data in Java.
To enable it, add the Play Ebean plugin to your SBT plugins in project/plugins.sbt
:
addSbtPlugin("io.github.lapidus79" % "sbt-play-ebean" % "6.2.2")
See latest version here.
Enable this plugin in the build.sbt
:
lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean)
Configure conf/application.conf
Play Ebean comes with two components, a runtime library that actually talks to the database, and an sbt plugin that enhances the compiled Java bytecode of your models for use with Ebean. Both of these components need to be configured so that Ebean knows where your models are.
Configuring the runtime library
The runtime library can be configured by putting the list of packages and/or classes that your Ebean models live in your application configuration file.
Since all our models are in the models package, add the following to conf/application.conf
:
ebean.default = ["models.*"]
This defines a default
Ebean server, using the default
data source, which must be properly configured.
Configuring the default
Ebean Datasource
1db {
2 # You can declare as many datasources as you want.
3 # By convention, the default datasource is named `default`
4
5 # https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database
6 default.driver = org.h2.Driver
7 default.url = "jdbc:h2:mem:play"
8 default.username = sa
9 default.password = ""
10 default.logSql=true
11}
Add the h2 database into your dependencies list in the build.sbt
:
libraryDependencies ++= Seq("com.h2database" % "h2" % "2.1.212")
Managing database evolutions
When you use a relational database, you need a way to track and organize your database schema evolutions.
Enable the evolutions by adding the following to the conf/application.conf
:
1play.evolutions {
2 # You can disable evolutions for a specific datasource if necessary
3 db.default.enabled = true
4}
Add jdbc
and javaJdbc
into your dependencies list in the build.sbt
:
libraryDependencies ++= Seq(jdbc,javaJdbc)
Using Model superclass
Ebean defines a convenient superclass for your Ebean model classes, io.ebean.Model
. Let us enhance our model classes to extend this Models Superclass.
Also add the Entity, Id and other java.persistance
annotation properties.
models.Tasks
1@Entity
2public class Tasks extends Model {
3 @Id
4 @GeneratedValue(strategy = GenerationType.AUTO)
5 private Long systemTaskId;
6 private String title;
7 private String description;
8 private String status;
9
10 private Date dueDate;
11 private String dueDateStr;
12
13 private Date creationDate;
14 private String creationDateStr;
15
16 @JsonManagedReference
17 @OneToMany(mappedBy = "todoTask", fetch = FetchType.EAGER)
18 private Set<TodoTaskComments> todoTaskCommentsSet;
19
20 public static Finder<Long, Tasks> find = new Finder<>(Tasks.class);
21
22 //bolierplate Constructors, Getters and Setters, toString methods
23}
As you can see, we’ve added a find
static field, defining a Finder
for an entity of type Task
with a Long
identifier. This helper field is then used to simplify querying our model.
models.TodoTaskComments
1@Entity
2public class TodoTaskComments extends Model {
3 @Id
4 @GeneratedValue(strategy = GenerationType.AUTO)
5 private Long todoTaskCommentsId;
6
7 private String taskComments;
8 private Date creationDate;
9 private String creationDateStr;
10
11 @JsonBackReference
12 @ManyToOne(fetch = FetchType.EAGER)
13 @JoinColumn(name="systemTasksId", nullable=false)
14 private Tasks todoTask;
15
16 public static Finder<Long, TodoTaskComments> find = new Finder<>(TodoTaskComments.class);
17
18 //bolierplate Constructors, Getters and Setters, toString methods
19}
Update Service Class
In the previous article, we have used the Hashmaps in the Service Class, to store the data. Hence we had adapted all our action methods to be in sync with that.
Now that we are persisting the data, we will update the Service Class to Store, Read, Edit and Delete the data from the database.
Storing the new Task details
1public void createTask(Tasks task){
2 task.setDueDateStr(dateFormatter(task.getDueDate()));
3 task.setCreationDate(new Date());
4 task.setCreationDateStr(dateFormatter(task.getCreationDate()));
5 task.save();
6}
Fetching list of tasks
1public List<Tasks> getAllTasks(){
2 return Tasks.find.all();
3}
Fetching one task details
1public Tasks getTaskById(long id){
2 return Tasks.find.byId(id);
3}
Update the Task details
1public void updateTask (Tasks task, TodoTaskComments todoTaskComments) {
2 if(todoTaskComments.getTaskComments() != null && !todoTaskComments.getTaskComments().isEmpty()) {
3 todoTaskComments.setCreationDate(new Date());
4 todoTaskComments.setCreationDateStr(dateFormatter(todoTaskComments.getCreationDate()));
5 todoTaskComments.setTodoTask(task);
6 todoTaskComments.save();
7
8 Set<TodoTaskComments> todoTaskCommentsSet = task.getTodoTaskCommentsSet();
9 todoTaskCommentsSet.add(todoTaskComments);
10 task.setTodoTaskCommentsSet(todoTaskCommentsSet);
11 }
12 task.update();
13}
Delete the Task details
1public void deleteTask(long systemTaskId){
2 Tasks task = Tasks.find.byId(systemTaskId);
3 if(null != task) {
4 Set<TodoTaskComments> todoTaskCommentsSet = task.getTodoTaskCommentsSet();
5 if(todoTaskCommentsSet.size() > 0){
6 for(TodoTaskComments todoTaskComments: todoTaskCommentsSet){
7 todoTaskComments.delete();
8 }
9 }
10 task.delete();
11 }
12}
Conclusion
With this setup complete, we have come to the end of this article.
At the end of this article, we have seen how to enhance the application developed in the previous article, by:
- Refactoring the Views using Bootstrap and jQuery, so that the Views are presented in a better way.
- Connecting to the Database to persist the data, using Ebean ORM.
Complete code for this project can be found at GitHub here. Go ahead and clone it.
Instructions on how to clone and run the project are provided on the GitHub page.