Views

MVC Application using Java and Play Framework (Contd)

MVC Application using Java and Play Framework (Contd)
Page content

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

  1. 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>
  1. 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 and font-awesome.min.css files path to the href attribute of the link tags inside the head tag.
  • Add the jquery.min.js, popper.min.js and bootstrap.min.js files path to the src attribute of the script tags at the bottom of the body tag.
  • Place the popper.min.js and jquery.min.js before the bootstrap.min.js, as bootstrap.min.js requires jQuery for loading its contents.
  • It is always advisable to add the script tags inside body 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.

refactored-home-page.jpeg
Refactored Home page

Create Task Page

Click on the Create Task tab on the navigation menu, at the top.

refactored-create-page.jpeg
Refactored Create page

Fill in the details and click on “Create Task” button, to create new task.

refactored-home-page-after-create.jpeg
Refactored Home page after Task Creation

View Task Page

Click on the row to View the details.

refactored-view-page.jpeg
Refactored View Page

Edit/Update Task Page

For Editing the task details, there are 2 options:

  1. From the Homepage, click on Edit icon.
  2. From View page, click on Edit icon.
    refactored-edit-page.jpeg
    Refactored Edit Page
    refactored-view-page-with-comments.jpeg
    Refactored View Page with Comments

Delete Task

For Deleting the task, there are 2 options:

  1. From the Homepage, click on Delete icon.
  2. From View page, click on Delete icon.
    refactored-home-page-after-delete.jpeg
    Refactored Home page after Task Deletion

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.