CustomMenu

Showing posts with label Spring MVC. Show all posts
Showing posts with label Spring MVC. Show all posts

Thursday, May 15, 2014

Spring Thymeleaf Permissions CRUD - Part 2

This post builds on the code from part 1 of this series,Spring Thymeleaf Permissions CRUD - Part 1.  In the past post we updated the security configuration, created the Permission DTO and Permission controller.  In this post we will finish off the CRUD functionality for the Permission entity by creating the three Thymeleaf CRUD pages.


1. We will start by creating the list page, as we have done with all of the prior entity CRUD pages.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>
    <style>
        .dropdown-menu {
          min-width: 0px;
        }    
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{permission.list.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-xs-12">
            
                <h4 class="page-header" data-th-text="#{permission.list.table.title}">Configured Users</h4>
                <div class="table responsive">
                <table class="table table-striped table-bordered table-hover">
                    <thead>
                        <tr>
                            <th class="col-xs-1" data-th-text="#{permission.list.id.label}">Id</th>
                            <th class="col-xs-4" data-th-text="#{permission.list.permission.label}">PermissionName</th>
                            <th class="col-xs-1" data-th-each="role : ${allRoles}">
                                 <span data-th-text="${role.rolename}"></span>
                            </th>
                            <th class="col-xs-1" data-th-text="#{permission.list.actions.label}">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr data-th-each="permission : ${permissions}">
                            <td data-th-text="${permission.id}">1</td>
                            <td data-th-text="${permission.permissionname}">perm1</td>
                            <td th:if="${allRoles[0]}">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[0])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td th:if="${#lists.size(allRoles)} > 1">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[1])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td th:if="${#lists.size(allRoles)} > 2">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[2])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td th:if="${#lists.size(allRoles)} > 3">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[3])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td th:if="${#lists.size(allRoles)} > 4">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[4])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td th:if="${#lists.size(allRoles)} > 5">
                                <input th:if="${#lists.contains(permission.permRoles, allRoles[5])}" type="checkbox" 
                                    name="option1" checked="checked" disabled="disabled"></input>
                            </td>
                            <td>
                                <div class="btn-group">
                                  <button class="btn btn-warning btn-xs dropdown-toggle" 
                                        type="button" data-toggle="dropdown" data-th-text="#{permission.list.actions.label}">
                                    Actions<span class="caret"></span>
                                  </button>
                                  <ul class="dropdown-menu">
                                    <li>
                                        <a href="#" data-th-href="@{/permission/edit(id=${permission.id})}">
                                            <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;
                                            <span data-th-text="#{button.label.edit}">edit</span>
                                        </a>
                                    </li>
                                    <li>
                                        <a href="#" data-th-href="@{/permission/delete(id=${permission.id},phase=stage)}">
                                           <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;
                                           <span data-th-text="#{button.label.delete}">delete</span>
                                        </a>
                                    </li>
                                  </ul>
                                </div>                          
                            </td>
                        </tr>
                        <tr data-th-remove="all">
                            <td>2</td>
                            <td>perm_test</td>
                            <td>role1</td>
                            <td>
                                <div class="btn-group">
                                  <button class="btn btn-warning btn-xs dropdown-toggle" type="button" data-toggle="dropdown" 
                                        data-th-text="#{permission.list.actions.label}">
                                    Actions<span class="caret"></span>
                                  </button>
                                  <ul class="dropdown-menu">
                                    <li>
                                        <a href="#">
                                            <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;
                                            <span data-th-text="#{button.label.edit}">edit</span>
                                        </a>
                                    </li>
                                    <li>
                                        <a href="#">
                                           <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;
                                           <span data-th-text="#{button.label.delete}">delete</span>
                                        </a>
                                    </li>
                                  </ul>
                                </div>                          
                            </td>
                        </tr>
                    </tbody>
                </table>
                </div>
                <form class="form" action="#" data-th-action="@{/permission/add}" data-th-object="${permissionDTO}" method="post">
                <div class="table responsive">
                    <table class="no-border-on-me table ">
                        <thead>
                            <tr>
                                <th class="col-xs-2"></th>
                                <th class="col-xs-4" data-th-text="#{permission.list.permission.label}">Perm</th>
                                <th class="col-xs-6" data-th-text="#{permission.list.actions.label}">Action</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td><input type="text" hidden="hidden" data-th-field="*{id}"></input></td>
                                <td><input class="form-control" type="text" data-th-field="*{permissionname}" placeholder="Permission Name"></input></td>
                                <td>
                                    <button type="submit" class="btn btn-primary" data-th-text="#{button.label.add}">Add Permission</button>
                                </td>
                            </tr>
                            <tr>
                                <td class="col-xs-2"></td>
                                <td class="col-xs-4 text-danger" data-th-if="${#fields.hasErrors('permissionname')}" data-th-errors="*{permissionname}">permission error</td>
                                <td class="col-xs-6"></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
                </form>
                 <div class="alert alert-danger alert-dismissable" th:if="${error != null}">
                    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
                    <h4 data-th-text="${error}">Error!</h4>
                </div>
                <div class="alert alert-success alert-dismissable" th:if="${message != null}">
                    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
                    <h4 data-th-text="${message}">Success!</h4>
                </div>              
                
            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

        
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>
There are several lines highlighted in the code above.  Here is a bit more detail about those highlighted lines:
  • Lines 68 - 70: In this block of code we are displaying all of the roles as column headers.  This section should be limited to 6 roles as the detail below can only display six roles.  From a functional perspective, I didn't plan on having more than three roles, with a possibility for a fourth.  So, I figured a maximum of six in the display was more than enough.  This header should really have a cap of six too...so this is a refactoring opportunity for the future.
  • Lines 78, 82, 86, 90, 94, 98: First, these columns (td tags) will only be displayed if there is a header/role for this particular column.  If there are only three roles in the system, then only three role headers will print, resulting in three role columns.  These lines will be conditionally displayed as we loop through each of the permissions in the permissions table.  
  • Lines 78, 82, 86, 90, 94, 98: If the permission being displayed is associated with a role in the column (td tag), then a disabled check box will be displayed, otherwise nothing will be displayed in that column.  Each td block corresponds to a column, and each column corresponds to a specific role.  If you spend some time looking at this code, the functionality should become clear...if not, please leave a comment and I will try to elaborate.
  • Starting on line 155 is the form used to add a permission to the system.  This line also refers to the DTO used for adding the permission.  The line referring to the name of the permission is line number 168.


2. Next we move onto the Permission edit page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{permission.edit.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">

                <h4 class="page-header" data-th-text="#{permission.edit.head.title}">Edit Permission</h4>
                <div class="col-sm-2"></div>
                <div class="col-sm-6">
                    <form class="form-horizontal" action="#" data-th-action="@{/permission/edit}" data-th-object="${permissionDTO}" method="post">
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{permission.list.permission.label}">Permission</label>
                            <div class="col-sm-7">
                                <input type="text" hidden="hidden" data-th-value="*{id}" data-th-field="*{id}" ></input>
                                <input type="text" class="form-control" data-th-value="*{permissionname}" data-th-field="*{permissionname}" ></input>
                            </div>
                        </div>
                        <div class="form-group" th:each="role : ${allRoles} ">
                            <label class="col-sm-5 control-label" data-th-text="${role.rolename}">Role 1</label>
                            <div class="col-sm-7">
                                <input type="checkbox" th:field="*{permRoles}" th:value="${role.id}"></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-offset-5 col-sm-7" >
                                <button type="submit" class="btn btn-primary"        name="action" 
                                        data-th-value="#{button.action.save}"   data-th-text="#{button.label.save}"  >Save</button>
                                <button type="submit" class="btn btn-default active" name="action" 
                                        data-th-value="#{button.action.cancel}" data-th-text="#{button.label.cancel}">Cancel</button>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-offset-5 col-sm-7" >
                                <p class="text-danger" data-th-if="${#fields.hasErrors('permissionname')}" 
                                        data-th-errors="*{permissionname}">type error</p> 
                            </div>
                        </div>
                    </form>
                </div>
                <div class="col-sm-4"></div>
                    
            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>
Note the section highlighted in blue above. Recall that besides the DTO object passed to this page as part of the HTTP GET request, the list of all roles in the system is also passed. In the highlighted rows above, all of the roles in the system are displayed as part of the for-each loop. For each of these roles, a check box is displayed. If the role is associated with the permission being edited, then a check is placed in the box, otherwise the checkbox is left empty.

The other code in this edit page should look similar to all of the other edit pages we have created on this blog.


3. Lastly, we have the Permission delete page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{permission.delete.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">

                <h4 class="page-header" data-th-text="#{permission.delete.head.title}">Delete Strategy</h4> 
                <div class="col-sm-2"></div>
                <div class="col-sm-6">
                    <form class="form-horizontal" action="#" method="get">
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{permission.list.id.label}">Permission Id</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${permission.id}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{permission.list.permission.label}">Permission Name</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${permission.permissionname}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <div class="form-group" th:each="role : ${permission.permRoles} ">
                            <label class="col-sm-5 control-label" data-th-text="${role.rolename}">Role 1</label>
                            <div class="col-sm-7">
                                <input type="checkbox" name="option1" checked="checked" disabled="disabled"></input>
                            </div>
                        </div>
                    </form>
                    
                    <div class="form-horizontal">
                        <div class="form-group">
                            <label class="col-sm-5 control-label"></label>
                            <div class="col-sm-7" >
                                <a href="#" data-th-href="@{/permission/delete(id=${permission.id},phase=#{button.action.delete} )}">
                                <button type="button" class="btn btn-primary" data-th-text="#{button.label.delete}">Delete</button></a>
                                
                                <a href="#" data-th-href="@{/permission/delete(id=${permission.id},phase=#{button.action.cancel} )}">
                                <button type="button" class="btn btn-default active" data-th-text="#{button.label.cancel}">Cancel</button></a>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="col-sm-4"></div>

            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>
Similar to the edit page, the highlighted section above loops through all of the roles associated with this permission and displays only this subset of roles on this delete page.


4. Screenshots of the Permission CRUD pages are shown below.
permission-list.html

permission-edit.html

permission-delete.html


We now have working CRUD functionality for our Permission Entity.  In the next development post we will look at session timeout and login redirection.

Code at GitHub: https://github.com/dtr-trading/spring-ex17-crud-perm

Wednesday, May 14, 2014

Spring Thymeleaf Permissions CRUD - Part 1

This post will build on the last development series that began with Spring Thymeleaf Roles CRUD - Part 1.  In this post we will create the CRUD controller and pages for Permissions.  The Permissions maintenance pages will be a bit more complicated than the CRUD pages for the other entities.

1. To begin, we will update the security configuration file so that we can access the permissions pages.  The additional entry is highlighted in the code below.
package com.dtr.oas.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.exception.AccessDeniedExceptionHandler;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Autowired
    AccessDeniedExceptionHandler accessDeniedExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/error/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("ADMIN")
                .antMatchers("/role/**").hasRole("ADMIN")
                .antMatchers("/permission/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

2. As with the User CRUD pages, we will need a DTO to transfer state to the controller. This is required because Permission objects holds references to one or more Role objects. Since Java objects cannot be returned from the browser, we will use a Permission DTO that will hold references to Role Id numbers, which can be returned from a browser. The DTO is shown below.
package com.dtr.oas.controller;
import java.io.Serializable;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;

public class PermissionDTO implements Serializable {

    private static final long serialVersionUID = -2707641174043923410L;
    static Logger logger = LoggerFactory.getLogger(PermissionDTO.class);
    
    private int id;

    @NotNull(message = "{error.permission.permissionname.null}")
    @NotEmpty(message = "{error.permission.permissionname.empty}")
    @Size(max = 50, message = "{permission.permissionname.role.max}")
    private String permissionname;
    
    private List<Integer> permRoles;
    
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPermissionname() {
        return permissionname;
    }

    public void setPermissionname(String permissionname) {
        this.permissionname = permissionname;
    }

    public List<Integer> getPermRoles() {
        return permRoles;
    }

    public void setPermRoles(List<Integer> permRoles) {
        this.permRoles = permRoles;
    }

    @Override
    public String toString() {
        return String.format("%s(id=%d, permissionname='%s')", 
                this.getClass().getSimpleName(), 
                this.getId(), this.getPermissionname());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;

        if (o instanceof PermissionDTO) {
            final PermissionDTO other = (PermissionDTO) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getPermissionname(), other.getPermissionname());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId(), getPermissionname());
    }

}

3. The last Java file that we will create is the controller for the Permission object. The controller is shown below, with some of the more important details described after the code.
package com.dtr.oas.controller;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.dtr.oas.exception.DuplicatePermissionException;
import com.dtr.oas.exception.PermissionNotFoundException;
import com.dtr.oas.exception.RoleNotFoundException;
import com.dtr.oas.model.Permission;
import com.dtr.oas.model.Role;
import com.dtr.oas.service.PermissionService;
import com.dtr.oas.service.RoleService;

@Controller
@RequestMapping(value = "/permission")
@PreAuthorize("denyAll")
public class PermissionController {

    static Logger logger = LoggerFactory.getLogger(PermissionController.class);
    static String businessObject = "permission"; //used in RedirectAttributes messages 
    
    @Autowired
    private RoleService roleService;

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private MessageSource messageSource;

    @ModelAttribute("allRoles")
    @PreAuthorize("hasAnyRole('CTRL_PERM_LIST_GET','CTRL_PERM_EDIT_GET')")
    public List<Role> getAllRoles() {
        return roleService.getRoles();
    }

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_PERM_LIST_GET')")
    public String listPermissions(Model model) {
        logger.debug("IN: Permission/list-GET");

        List<Permission> permissions = permissionService.getPermissions();
        model.addAttribute("permissions", permissions);

        // if there was an error in /add, we do not want to overwrite
        // the existing user object containing the errors.
        if (!model.containsAttribute("permissionDTO")) {
            logger.debug("Adding PermissionDTO object to model");
            PermissionDTO permissionDTO = new PermissionDTO();
            model.addAttribute("permissionDTO", permissionDTO);
        }
        return "permission-list";
    }
    
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_PERM_ADD_POST')")
    public String addPermission(@Valid @ModelAttribute PermissionDTO permissionDTO,
            BindingResult result, RedirectAttributes redirectAttrs) {
        
        logger.debug("IN: Permission/add-POST");
        logger.debug("  DTO: " + permissionDTO.toString());

        if (result.hasErrors()) {
            logger.debug("PermissionDTO add error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.permissionDTO", result);
            redirectAttrs.addFlashAttribute("permissionDTO", permissionDTO);
            return "redirect:/permission/list";
        } else {
            Permission perm = new Permission();

            try {
                perm = getPermission(permissionDTO);
                permissionService.addPermission(perm);
                String message = messageSource.getMessage("ctrl.message.success.add", 
                        new Object[] {businessObject, perm.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/permission/list";
            } catch (DuplicatePermissionException e) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", 
                        new Object[] {businessObject, permissionDTO.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
           } catch (RoleNotFoundException e) {
               String message = messageSource.getMessage("ctrl.message.error.notfound", 
                       new Object[] {"role ids", permissionDTO.getPermRoles().toString()}, Locale.US);
               redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
            }
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_PERM_EDIT_POST')")
    public String editPermission(@Valid @ModelAttribute PermissionDTO permissionDTO,
            BindingResult result, RedirectAttributes redirectAttrs,
            @RequestParam(value = "action", required = true) String action) {

        logger.debug("IN: Permission/edit-POST: " + action);

        if (action.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = messageSource.getMessage("ctrl.message.success.cancel", 
                    new Object[] {"Edit", businessObject, permissionDTO.getPermissionname()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
        } else if (result.hasErrors()) {
            logger.debug("Permission-edit error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.permissionDTO", result);
            redirectAttrs.addFlashAttribute("permissionDTO", permissionDTO);
            return "redirect:/permission/edit?id=" + permissionDTO.getId();
        } else if (action.equals(messageSource.getMessage("button.action.save",  null, Locale.US))) {
            logger.debug("Permission/edit-POST:  " + permissionDTO.toString());
            try {
                Permission permission = getPermission(permissionDTO);
                permissionService.updatePermission(permission);
                String message = messageSource.getMessage("ctrl.message.success.update", 
                        new Object[] {businessObject, permissionDTO.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
            } catch (DuplicatePermissionException unf) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", 
                        new Object[] {businessObject, permissionDTO.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
            } catch (PermissionNotFoundException unf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", 
                        new Object[] {businessObject, permissionDTO.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
            } catch (RoleNotFoundException unf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", 
                        new Object[] {"role ids", permissionDTO.getPermRoles().toString()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
            }
        }
        return "redirect:/permission/list";
    }
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_PERM_EDIT_GET')")
    public String editPermissionPage(@RequestParam(value = "id", required = true)
            Integer id, Model model, RedirectAttributes redirectAttrs) {

        logger.debug("IN: Permission/edit-GET:  ID to query = " + id);

        try {
            if (!model.containsAttribute("permissionDTO")) {
                logger.debug("Adding permissionDTO object to model");
                Permission perm = permissionService.getPermission(id);
                PermissionDTO permissionDTO = getPermissionDTO(perm);
                logger.debug("Permission/edit-GET:  " + permissionDTO.toString());
                model.addAttribute("permissionDTO", permissionDTO);
            }
            return "permission-edit";
        } catch (PermissionNotFoundException e) {
            String message = messageSource.getMessage("ctrl.message.error.notfound", 
                    new Object[] {"user id", id}, Locale.US);
            model.addAttribute("error", message);
            return "redirect:/permission/list";
        }
    }

    
    
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_PERM_DELETE_GET')")
    public String deletePermission(
            @RequestParam(value = "id", required = true) Integer id,
            @RequestParam(value = "phase", required = true) String phase,
            Model model, RedirectAttributes redirectAttrs) {

        Permission permission;
        try {
            permission = permissionService.getPermission(id);
        } catch (PermissionNotFoundException e) {
            String message = messageSource.getMessage("ctrl.message.error.notfound", 
                    new Object[] {"permission id", id}, Locale.US);
            redirectAttrs.addFlashAttribute("error", message);
            return "redirect:/permission/list";
        }

        logger.debug("IN: Permission/delete-GET | id = " + id + " | phase = " + phase + " | " + permission.toString());

        if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = messageSource.getMessage("ctrl.message.success.cancel", 
                    new Object[] {"Delete", businessObject, permission.getPermissionname()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
            return "redirect:/permission/list";
        } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) {
            logger.debug("     deleting permission : " + permission.toString());
            model.addAttribute("permission", permission);
            return "permission-delete";
        } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) {
            try {
                permissionService.deletePermission(permission.getId());
                String message = messageSource.getMessage("ctrl.message.success.delete", 
                        new Object[] {businessObject, permission.getPermissionname()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/permission/list";
            } catch (PermissionNotFoundException e) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", 
                        new Object[] {"permission id", id}, Locale.US);
               redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/permission/list";
           }
        }

        return "redirect:/permission/list";
    }

    @PreAuthorize("hasAnyRole('CTRL_PERM_EDIT_GET','CTRL_PERM_DELETE_GET')")
    public PermissionDTO getPermissionDTO(Permission perm) {
        List<Integer> roleIdList = new ArrayList<Integer>();
        PermissionDTO permDTO = new PermissionDTO();
        permDTO.setId(perm.getId());
        permDTO.setPermissionname(perm.getPermissionname());
        for (Role role : perm.getPermRoles()) {
            roleIdList.add(role.getId());
        }
        permDTO.setPermRoles(roleIdList);
        return permDTO;
    }

    @PreAuthorize("hasAnyRole('CTRL_PERM_ADD_POST','CTRL_PERM_EDIT_POST')")
    public Permission getPermission(PermissionDTO permissionDTO) throws RoleNotFoundException {
        Set<Role> roleList = new HashSet<Role>();
        Permission perm = new Permission();
        Role role = new Role();
        
        perm.setId(permissionDTO.getId());
        perm.setPermissionname(permissionDTO.getPermissionname());
        if (permissionDTO.getPermRoles() != null) {
            for (Integer roleId : permissionDTO.getPermRoles()) {
                role = roleService.getRole(roleId);
                logger.debug("  ROLE: " + role.toString());
                roleList.add(role);
            }
            perm.setPermRoles(roleList);
        }
        logger.debug("  PERM: " + perm.toString());
        return perm;
    }
}
Similar to the User controller, we are autowiring two services: one for the PermissionService and one for the RoleService. Permission maintenance will provide the capability to add/update/delete Roles associated with a permission.

The list, add, edit, and delete methods look nearly identical to the same methods on the User controller, and were discussed in the User CRUD posts. Note that the Permission add and edit pages have to handle an exception thrown if a role is not found...not very likely, but it needs to be handled.

At the bottom of the conroller code are methods to convert DTOs to Entities, and Entities to DTOs. The functionality to notice is the handling of the Permission objects associations with Role objects.

In part 2, we will create the three Thymeleaf pages and show some screenshots of the Permissions CRUD pages.


Wednesday, May 7, 2014

Spring Thymeleaf Roles CRUD - Part 2

This post builds on the code from part 1 of this series,Spring Thymeleaf Roles CRUD - Part 1.  In the past post we refactored some code and bulit the role controller.  In this post we will finish off the CRUD functionality for the Role entity by creating the Thymeleaf CRUD pages.


1. Before I show the Thymeleaf pages for managing roles, there were two updates to the user pages and user controller to handle the situation where a user is not assigned to a role.  Here are the relevant user code fragments with the changes highlighted in blue.
    ...
    @PreAuthorize("hasAnyRole('CTRL_USER_EDIT_GET','CTRL_USER_DELETE_GET')")
    public UserDTO getUserDTO(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setUsername(user.getUsername());
        userDTO.setPassword(user.getPassword());
        userDTO.setEnabled(user.getEnabled());
        
        Role role = new Role();
        if (user.getRole() == null || user.getRole().getId() == 0) {
            role = setNullRole();
        } else {
            role = user.getRole();
        }
        userDTO.setRoleId(role.getId());
        return userDTO;
    }

    @PreAuthorize("hasAnyRole('CTRL_USER_ADD_POST','CTRL_USER_EDIT_POST')")
    public User getUser(UserDTO userDTO) {
        User user = new User();
        user.setId(userDTO.getId());

        Role role = new Role();
        try {
            role = roleService.getRole(userDTO.getRoleId());
        } catch (RoleNotFoundException e) {
            role = setNullRole();
        }
        user.setRole(role);

        user.setUsername(userDTO.getUsername());
        user.setPassword(userDTO.getPassword());
        user.setEnabled(userDTO.getEnabled());
        return user;
    }
    
    @PreAuthorize("hasAnyRole('CTRL_USER_EDIT_GET','CTRL_USER_DELETE_GET','CTRL_USER_ADD_POST','CTRL_USER_EDIT_POST')")
    public Role setNullRole() {
        Role role = new Role();
        role.setId(0);
        role.setRolename("ROLE_NOT_SET");
        return role;
    }
    ...
    ...
    <tr data-th-each="user : ${users}">
        <td data-th-text="${user.id}">1</td>
        <td data-th-text="${user.username}">user1</td>
        <td data-th-text="${user.password}">password1</td>
        <td data-th-text="${user.enabled}">true</td>
        <td data-th-text="(${user.role == null} ? null : ${user.role.rolename})">ROLE_TEST1</td>
        <td>
    ...
    ...
         <div class="form-group">
            <label class="col-sm-5 control-label" data-th-text="#{user.list.role.label}">Role</label>
            <div class="col-sm-7">
                <input type="text" class="form-control" data-th-value="(${user.role == null} ? null : ${user.role.rolename})" disabled="disabled" ></input>
            </div>
        </div>
    </form>
    ...

2. Next we create the role list page.  The first highlighted line below is showing the loop that will display all of  the roles in tabular format.  The second highlighted line shows the form for adding new roles, and the DTO object used for adding.  This page should look pretty similar to the strategy list and user list pages.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>
    <style>
        .dropdown-menu {
          min-width: 0px;
        }    
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{role.list.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-xs-12">
            
                <h4 class="page-header" data-th-text="#{role.list.table.title}">Configured Strategies</h4>
                <div class="table responsive">
                <table class="table table-striped table-bordered table-hover">
                    <thead>
                        <tr>
                            <th class="col-xs-1" data-th-text="#{role.list.id.label}">Id</th>
                            <th class="col-xs-10" data-th-text="#{role.list.rolename.label}">Strategy Type</th>
                            <th class="col-xs-1" data-th-text="#{role.list.actions.label}">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr data-th-each="role : ${roles}">
                            <td data-th-text="${role.id}">1</td>
                            <td data-th-text="${role.rolename}">USER_TEST</td>
                            <td>
                                <div class="btn-group">
                                  <button type="button" class="btn btn-warning btn-xs dropdown-toggle" data-toggle="dropdown">
                                    Actions<span class="caret"></span>
                                  </button>
                                  <ul class="dropdown-menu">
                                    <li>
                                        <a href="#" data-th-href="@{/role/edit(id=${role.id})}">
                                            <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;
                                            <span data-th-text="#{button.label.edit}">edit</span>
                                       </a>
                                    </li>
                                    <li>
                                        <a href="#" data-th-href="@{/role/delete(id=${role.id},phase=stage)}">
                                           <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;
                                           <span data-th-text="#{button.label.delete}">delete</span>
                                       </a>
                                    </li>
                                  </ul>
                                </div>                            
                            </td>
                        </tr>
                        <tr data-th-remove="all">
                            <td>2</td>
                            <td>ROLE_TEST2</td>
                            <td>
                                <div class="btn-group">
                                  <button type="button" class="btn btn-warning btn-xs dropdown-toggle" data-toggle="dropdown">
                                    Actions<span class="caret"></span>
                                  </button>
                                  <ul class="dropdown-menu">
                                    <li>
                                        <a href="#">
                                            <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;
                                            <span data-th-text="#{button.label.edit}">edit</span>
                                       </a>
                                    </li>
                                    <li>
                                        <a href="#">
                                           <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;
                                           <span data-th-text="#{button.label.delete}">delete</span>
                                       </a>
                                    </li>
                                  </ul>
                                </div>                            
                            </td>
                        </tr>
                    </tbody>
                </table>
                </div>
                
                <form class="form" action="#" data-th-action="@{/role/add}" data-th-object="${role}" method="post">
                <div class="table responsive">
                    <table class="no-border-on-me table ">
                        <thead>
                            <tr>
                                <th class="col-xs-1"></th>
                                <th class="col-xs-10" data-th-text="#{role.list.rolename.label}">Strategy Name</th>
                                <th class="col-xs-1" data-th-text="#{role.list.actions.label}">Action</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td><input type="text" hidden="hidden" data-th-field="*{id}"></input></td>
                                <td><input class="form-control" type="text" data-th-field="*{rolename}" placeholder="Name"></input></td>
                                <td>
                                    <button type="submit" class="btn btn-primary" data-th-text="#{button.label.add}">Add Role</button>
                                </td>
                            </tr>
                            <tr>
                                <td class="col-xs-1"></td>
                                <td class="col-xs-10 text-danger" data-th-if="${#fields.hasErrors('rolename')}" data-th-errors="*{rolename}">name error</td>
                                <td class="col-xs-1"></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
                </form>
                
                 <div class="alert alert-danger alert-dismissable" th:if="${error != null}">
                    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
                    <h4 data-th-text="${error}">Error!</h4>
                </div>
                <div class="alert alert-success alert-dismissable" th:if="${message != null}">
                    <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
                    <h4 data-th-text="${message}">Success!</h4>
                </div>              
                
                
            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

        
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>

3. The role edit page is shown below.  This page is very similar to the strategy edit page discussed in the past.  The start of the form is highlighted in blue.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{role.edit.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">

                <h4 class="page-header" data-th-text="#{role.edit.head.title}">Edit Strategy</h4>
                <div class="col-sm-2"></div>
                <div class="col-sm-6">
                    <form class="form-horizontal" action="#" data-th-action="@{/role/edit}" data-th-object="${role}" method="post">
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{role.list.rolename.label}">Strategy Type</label>
                            <div class="col-sm-7">
                                <input type="text" hidden="hidden" data-th-value="*{id}" data-th-field="*{id}" ></input>
                                <input type="text" class="form-control" data-th-value="*{rolename}" data-th-field="*{rolename}" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-offset-5 col-sm-7" >
                                <button type="submit" class="btn btn-primary"        name="action" data-th-value="#{button.action.save}"   data-th-text="#{button.label.save}"  >Save</button>
                                <button type="submit" class="btn btn-default active" name="action" data-th-value="#{button.action.cancel}" data-th-text="#{button.label.cancel}">Cancel</button>
                            </div>
                        </div>
                        <div class="form-group">
                            <div class="col-sm-offset-5 col-sm-7" >
                                <p class="text-danger" data-th-if="${#fields.hasErrors('rolename')}" data-th-errors="*{rolename}">name error</p> 
                            </div>
                        </div>
                    </form>
                </div>
                <div class="col-sm-4"></div>
                    
            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>

            

4.  Finally the delete page is shown.  By now you should be feeling pretty comfortable with creating Thymeleaf CRUD pages.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <!-- Core CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" 
        data-th-href="@{/resources/css/bootstrap-3.1.1.min.css}" />
        
    <link type="text/css" rel="stylesheet" href="../../resources/font-awesome/css/font-awesome.css" 
        data-th-href="@{/resources/font-awesome/css/font-awesome.css}" />

    <!-- SB Admin CSS - Include with every page -->
    <link type="text/css" rel="stylesheet" href="../../resources/css/sb-admin.css" 
        data-th-href="@{/resources/css/sb-admin.css}" />
    
    <style>
        .no-border-on-me>thead>tr>th,
        .no-border-on-me>tbody>tr>th,
        .no-border-on-me>tfoot>tr>th,
        .no-border-on-me>thead>tr>td,
        .no-border-on-me>tbody>tr>td,
        .no-border-on-me>tfoot>tr>td
        {
            border-top-style: none;
            border-bottom-style: none;
        }
    </style>

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
          <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
          <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
        <![endif]-->

    <title data-th-text="#{role.delete.page.title}">Title</title>
</head>

<body>

<div id="wrapper">                <!-- /#wrapper -->

    <div data-th-replace="fragments/sb-admin :: top-nav"></div>
    
    <div data-th-replace="fragments/sb-admin :: vert-nav"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">

                <h4 class="page-header" data-th-text="#{role.delete.head.title}">Delete Role</h4> 
                <div class="col-sm-2"></div>
                <div class="col-sm-6">
                    <form class="form-horizontal" action="#" method="get">
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{role.list.id.label}">Role Id</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-field="${role.id}" disabled="disabled"></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{role.list.rolename.label}">Role Name</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-field="${role.rolename}" disabled="disabled"></input>
                            </div>
                        </div>
                    </form>
                    <div class="form-horizontal">
                        <div class="form-group">
                            <label class="col-sm-5 control-label"></label>
                            <div class="col-sm-7" >
                                <a href="#" data-th-href="@{/role/delete(id=${role.id},phase=#{button.action.delete} )}">
                                <button type="button" class="btn btn-primary" data-th-text="#{button.label.delete}">Delete</button></a>
                                
                                <a href="#" data-th-href="@{/role/delete(id=${role.id},phase=#{button.action.cancel} )}">
                                <button type="button" class="btn btn-default active" data-th-text="#{button.label.cancel}">Cancel</button></a>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="col-sm-4"></div>

            </div>  <!-- /.col-lg-12 -->                
        </div>      <!-- /.row -->              
    </div>          <!-- page wrapper -->
</div>              <!-- /#wrapper -->

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js" 
        data-th-src="@{/resources/js/jquery-1.11.0.min.js}"></script>
        
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script type="text/javascript" src="../../resources/js/bootstrap-3.1.1.min.js" 
        data-th-src="@{/resources/js/bootstrap-3.1.1.min.js}"></script>

    <!-- Core Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js" 
        data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}"></script>
        
    <!-- SB Admin Scripts - Include with every page -->
    <script type="text/javascript" src="../../resources/js/sb-admin.js" 
        data-th-src="@{/resources/js/sb-admin.js}"></script>

</body>
</html>

5. The application now has working role CRUD functionality. Here are a few screenshots of the running application.
Role list / add page

Role edit page

Role delete page


We now have working CRUD functionality for our Role Entity. In the next series, we will add CRUD functionality to our Permission Entity. The list/add and edit pages for the Permission Entity will be a bit different than similar pages in the past, so I will discuss the modifications to those pages.

Code at GitHub: https://github.com/dtr-trading/spring-ex16-crud

Monday, May 5, 2014

Spring Thymeleaf Roles CRUD - Part 1

In this two part post, we will create CRUD pages for the Role entity.  In this first part we will create the controller, and make some defect fixes in the model and DAO layers.  In order to access this new controller we will need to update the security configuration file also.  Lastly, this post builds on the code created in the series of posts starting with Spring Thymeleaf CRUD User Maintenance - Part 1.


1. Let's start with some updates to the Permission and Strategy entities.  The changes are highlighted in the code fragments below.
...
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;

        if (o instanceof Permission) {
            final Permission other = (Permission) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getPermissionname(), other.getPermissionname());
        }
        return false;
    }
...
...
public class Strategy extends BaseEntity implements Serializable {

    private static final long serialVersionUID = 96285180113476324L;
    static Logger logger = LoggerFactory.getLogger(Strategy.class);

    @NotNull(message = "{error.strategy.type.null}")
    @NotEmpty(message = "{error.strategy.type.empty}")
    @Size(max = 50, message = "{error.strategy.type.max}")
    @Column(name = "TYPE", length = 50)
    private String type;

    @NotNull(message = "{error.strategy.name.null}")
    @NotEmpty(message = "{error.strategy.name.empty}")
    @Size(max = 50, message = "{error.strategy.name.max}")
    @Column(name = "NAME", length = 50)
    private String name;
...
The defect in the first fragment (Permission.java) was related to an incorrect Object comparison, due to copy-paste. The second change (in Strategy.java) was to increase the field size in both the entity and database DDL.


2. In the DAO layer, I discovered some defects related to the update methods, specifically around an update creating a duplicate entry. Also, I've changed the getList methods on the DAOs to return sorted lists. The relevant updates are in the fragments below.
...
    @Override
    public void updateStrategy(Strategy strategy) throws StrategyNotFoundException, DuplicateStrategyException {
        Strategy strategyToUpdate = getStrategy(strategy.getId());
        
        // iterated to see if this update is a duplicate
        List<Strategy> strategies = getStrategies();
        for (Strategy singleStrategy : strategies) {
            if (singleStrategy.getType().equals(strategy.getType()) && singleStrategy.getName().equals(strategy.getName())) {
                String message = "The Strategy [" + singleStrategy.getName() + "] already exists in the system.";
                throw new DuplicateStrategyException(message);
            }
        }
        
        // no exception thrown, so it is not a duplicate
        strategyToUpdate.setName(strategy.getName());
        strategyToUpdate.setType(strategy.getType());
        getCurrentSession().update(strategyToUpdate);
    }

...

    @Override
    @SuppressWarnings("unchecked")
    public List<Strategy> getStrategies() {
        String hql = "FROM Strategy s ORDER BY s.id";
        return getCurrentSession().createQuery(hql).list();
    }
...
...
@Override
    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException {
        User userToUpdate = getUser(user.getId());
        
        try {
            User userCheck = getUser(user.getUsername());
            if (userCheck.getId() == userToUpdate.getId()) {
                userToUpdate.setEnabled(user.getEnabled());
                userToUpdate.setPassword(user.getPassword());
                userToUpdate.setUsername(user.getUsername());
                userToUpdate.setRole(user.getRole());
                getCurrentSession().update(userToUpdate);
            } else {
                String message = "The user [" + userCheck.getUsername() + "] already exists";
                throw new DuplicateUserException(message);
            }
        } catch (UserNotFoundException e) {
            userToUpdate.setEnabled(user.getEnabled());
            userToUpdate.setPassword(user.getPassword());
            userToUpdate.setUsername(user.getUsername());
            userToUpdate.setRole(user.getRole());
            getCurrentSession().update(userToUpdate);
        }
    }

...

    @Override
    @SuppressWarnings("unchecked")
    public List<User> getUsers() {
        String hql = "FROM User u ORDER BY u.id";
        return getCurrentSession().createQuery(hql).list();
    }
...
...
    @Override
    public void updateRole(Role role) throws RoleNotFoundException, DuplicateRoleException {
        Role roleToUpdate = getRole(role.getId());
        
        try {
            Role roleCheck = getRole(role.getRolename());
            if (roleCheck.getId() == roleToUpdate.getId()) {
                roleToUpdate.setId(role.getId());
                roleToUpdate.setRolename(role.getRolename());
            } else {
                String message = "The role [" + roleCheck.getRolename() + "] already exists";
                throw new DuplicateRoleException(message);
            }
        } catch (RoleNotFoundException e) {
            roleToUpdate.setId(role.getId());
            roleToUpdate.setRolename(role.getRolename());
            getCurrentSession().update(roleToUpdate);
        }
    }

...

    @Override
    @SuppressWarnings("unchecked")
    public List<Role> getRoles() {
        String hql = "FROM Role r ORDER BY r.id";
        return getCurrentSession().createQuery(hql).list();
    }
...
...
    @Override
    public void updatePermission(Permission permission) throws PermissionNotFoundException, DuplicatePermissionException {
        Permission permToUpdate = getPermission(permission.getId());
        
        try {
            Permission permissionCheck = getPermission(permission.getPermissionname());
            if (permToUpdate.getId() == permissionCheck.getId()) {
                permToUpdate.setId(permission.getId());
                permToUpdate.setPermissionname(permission.getPermissionname());
                permToUpdate.setPermRoles(permission.getPermRoles());
                getCurrentSession().update(permToUpdate);
            } else {
                String message = "The permission [" + permissionCheck.getPermissionname() + "] already exists";
                throw new DuplicatePermissionException(message);
            }
        } catch (PermissionNotFoundException e) {
            permToUpdate.setId(permission.getId());
            permToUpdate.setPermissionname(permission.getPermissionname());
            permToUpdate.setPermRoles(permission.getPermRoles());
            getCurrentSession().update(permToUpdate);
        }
    }



    @Override
    @SuppressWarnings("unchecked")
    public List<Permission> getPermissions() {
        String hql = "FROM Permission p ORDER BY p.id";
        return getCurrentSession().createQuery(hql).list();
    }
...

3. Now that we're finished with the refactoring and defects, we will update the security configuration to allow access to the Role controller methods.
package com.dtr.oas.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.dtr.oas.exception.AccessDeniedExceptionHandler;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Autowired
    AccessDeniedExceptionHandler accessDeniedExceptionHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .antMatchers("/error/**").permitAll()
                .antMatchers("/strategy/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("ADMIN")
                .antMatchers("/role/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

4. We're ready for the Role controller now. I won't discuss this controller, since this one should look pretty similar to the Strategy and User controllers that we created in past posts.
package com.dtr.oas.controller;
import java.util.List;
import java.util.Locale;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.dtr.oas.exception.DuplicateRoleException;
import com.dtr.oas.exception.RoleNotFoundException;
import com.dtr.oas.model.Role;
import com.dtr.oas.service.RoleService;

@Controller
@RequestMapping(value = "/role")
@PreAuthorize("denyAll")
public class RoleController {

    static Logger logger = LoggerFactory.getLogger(RoleController.class);
    static String businessObject = "role"; //used in RedirectAttributes messages 

    @Autowired
    private RoleService roleService;

    @Autowired
    private MessageSource messageSource;
    
    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_ROLE_LIST_GET')")
    public String listOfRoles(Model model) {
        logger.debug("IN: Role/list-GET");

        List<Role> roles = roleService.getRoles();
        model.addAttribute("roles", roles);

        // if there was an error in /add, we do not want to overwrite
        // the existing role object containing the errors.
        if (!model.containsAttribute("role")) {
            logger.debug("Adding Role object to model");
            Role role = new Role();
            model.addAttribute("role", role);
        }
        return "role-list";
    }              
    
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_ROLE_ADD_POST')")
    public String addRole(@Valid @ModelAttribute Role role,
            BindingResult result, RedirectAttributes redirectAttrs) {

        logger.debug("IN: Role/add-POST");

        if (result.hasErrors()) {
            logger.debug("Role-add error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.role", result);
            redirectAttrs.addFlashAttribute("role", role);
            return "redirect:/role/list";
        } else {
            try {
                roleService.addRole(role);
                String message = messageSource.getMessage("ctrl.message.success.add", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/role/list";
            } catch (DuplicateRoleException e) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/role/list";
            }
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_ROLE_EDIT_GET')")
    public String editRolePage(@RequestParam(value = "id", required = true) 
            Integer id, Model model, RedirectAttributes redirectAttrs) {
        
        logger.debug("IN: Role/edit-GET:  ID to query = " + id);

        try {
            if (!model.containsAttribute("role")) {
                logger.debug("Adding Role object to model");
                Role role = roleService.getRole(id);
                logger.debug("Role/edit-GET:  " + role.toString());
                model.addAttribute("role", role);
            }
            return "role-edit";
        } catch (RoleNotFoundException e) {
            String message = messageSource.getMessage("ctrl.message.error.notfound", 
                    new Object[] {"role id", id}, Locale.US);
            model.addAttribute("error", message);
            return "redirect:/role/list";
        }
    }
        
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_ROLE_EDIT_POST')")
    public String editRole(@Valid @ModelAttribute Role role,
            BindingResult result, RedirectAttributes redirectAttrs,
            @RequestParam(value = "action", required = true) String action) {

        logger.debug("IN: Role/edit-POST: " + action);

        if (action.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = messageSource.getMessage("ctrl.message.success.cancel", 
                    new Object[] {"Edit", businessObject, role.getRolename()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
        } else if (result.hasErrors()) {
            logger.debug("Role-edit error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.role", result);
            redirectAttrs.addFlashAttribute("role", role);
            return "redirect:/role/edit?id=" + role.getId();
        } else if (action.equals(messageSource.getMessage("button.action.save",  null, Locale.US))) {
            logger.debug("Role/edit-POST:  " + role.toString());
            try {
                roleService.updateRole(role);
                String message = messageSource.getMessage("ctrl.message.success.update", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
            } catch (RoleNotFoundException snf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/role/list";
            } catch (DuplicateRoleException dse) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/role/list";
            }
        }
        return "redirect:/role/list";
    }

    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_ROLE_DELETE_GET')")
    public String deleteRolePage(
            @RequestParam(value = "id", required = true) Integer id,
            @RequestParam(value = "phase", required = true) String phase,
            Model model, RedirectAttributes redirectAttrs) {

        Role role;
        String message;
        
        try {
            role = roleService.getRole(id);
        } catch (RoleNotFoundException e) {
            message = messageSource.getMessage("ctrl.message.error.notfound", 
                    new Object[] {"Role number", id}, Locale.US);
            redirectAttrs.addFlashAttribute("error", message);
            return "redirect:/role/list";
        }

        logger.debug("IN: Role/delete-GET | id = " + id + " | phase = " + phase + " | " + role.toString());

        if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            message = messageSource.getMessage("ctrl.message.success.cancel", 
                    new Object[] {"Delete", businessObject, role.getRolename()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
            return "redirect:/role/list";
        } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) {
            model.addAttribute("role", role);
            return "role-delete";
        } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) {
            try {
                roleService.deleteRole(id);
                message = messageSource.getMessage("ctrl.message.success.delete", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/role/list";
            } catch (RoleNotFoundException e) {
                message = messageSource.getMessage("ctrl.message.error.notfound", 
                        new Object[] {businessObject, role.getRolename()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/role/list";
            }
        }
        return "redirect:/role/list";
    }
    
}

In part two we will create the three Thymeleaf html pages that are needed for the Role CRUD functionality.