CustomMenu

Showing posts with label Hibernate. Show all posts
Showing posts with label Hibernate. Show all posts

Wednesday, April 30, 2014

Spring Thymeleaf CRUD User Maintenance - Part 3

This post builds on the code from part 2 of this series,Spring Thymeleaf CRUD User Maintenance - Part 2.  In this post, we will create the Thymeleaf CRUD pages, update the menus, and update the Twitter Bootstrap functionality (associated with alerts and actions menus).

The basic flow of the user pages will be the same as the flow of the strategy pages.  Selecting the user page link will take you to the user list page.  This page will display all users in the system and contain a form at the bottom of the page to add new users.  Each row in the list, will provide an action menu to edit or delete a user.  Success and failure of the three actions (add, edit, or delete) will return the user to the list page with a message displaying the action status.  Besides the list page, we will create edit and delete pages as well, and update the common Thymeleaf fragment page.

1. Let's start by updating the Thymeleaf fragment.
<!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 >SB-Admin</title>   
</head>

<body>
    <div id="wrapper">

        <nav data-th-fragment="top-nav" class="navbar navbar-default navbar-static-top" style="margin-bottom: 0">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".sidebar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#" data-th-href="@{/}">Option Algo System</a>
            </div>
            <!-- /.navbar-header -->

            <ul class="nav navbar-top-links navbar-right" data-th-with="currentUser=${#httpServletRequest.userPrincipal?.name}">
                    <li><a href="#" data-th-href="@{/}"            >Home</a></li>
                    <li><a href="#" data-th-href="@{/trading}"     >Trading</a></li>
                    <li><a href="#" data-th-href="@{/backtesting}" >Backtesting</a></li>
                    <li><a href="#" data-th-href="@{/admin}"       >Admin</a></li>
                    <li class="dropdown" data-th-if="${currentUser != null}">
                            <a class="dropdown-toggle" data-toggle="dropdown" href="#">
                                <i class="fa fa-user fa-fw"></i>
                                <font color="#049cbd" th:text="'&nbsp;' + ${currentUser} + '&nbsp;&nbsp;'">&nbsp;Dave&nbsp;&nbsp;</font>
                                <i class="fa fa-caret-down"></i>
                            </a>
                            <ul class="dropdown-menu dropdown-user">
                                <li><a href="#"><i class="fa fa-user fa-fw"></i>User Profile</a></li>
                                <li><a href="#"><i class="fa fa-gear fa-fw"></i>Settings</a></li>
                                <li class="divider"></li>
                                <li>
                                    <form class="navbar-form" data-th-action="@{/logout}" method="post">
                                       <label for="mySubmit" class="btn"><i class="fa fa-sign-out fa-fw"></i>Log Out</label>
                                       <input id="mySubmit" type="submit" value="Go" class="hidden" />
                                    </form>
                                </li>
                            </ul>
                            <!-- /.dropdown-user -->
                    </li>
            </ul>  <!-- /.navbar-top-links -->
        </nav>     <!-- /.navbar-static-top -->

        <nav data-th-fragment="vert-nav" class="navbar-default navbar-static-side" >
            <div class="sidebar-collapse">
                <ul class="nav" id="side-menu">
                    <li th:class="${(#httpServletRequest.requestURL.toString().contains('user'))} or
                                    ${(#httpServletRequest.requestURL.toString().contains('role'))} or
                                    ${(#httpServletRequest.requestURL.toString().contains('permission'))} ? 'active'">
                        <a href="#" data-th-href="@{/user/list}"><i class="fa fa-lock fa-fw"></i> Security<span class="fa arrow"></span></a>
                        <ul class="nav nav-second-level">
                            <li>
                                <a href="#" data-th-href="@{/user/list}">User List</a>
                            </li>
                            <li>
                                <a href="#" data-th-href="@{/role/list}">Role List</a>
                            </li>
                            <li>
                                <a href="#" data-th-href="@{/permission/list}">Permission List</a>
                            </li>
                        </ul>
                        <!-- /.nav-second-level -->
                    </li>
                    <li th:class="${(#httpServletRequest.requestURL.toString().contains('account'))} ? 'active'">
                        <a href="#" data-th-href="@{/account/list}"><i class="fa fa-user fa-fw"></i> Accounts<span class="fa arrow"></span></a>
                        <ul class="nav nav-second-level">
                            <li>
                                <a href="#" data-th-href="@{/account/list}">Account List</a>
                            </li>
                            <li>
                                <a href="#" data-th-href="@{/account-allocation/list}">Allocation List</a>
                            </li>
                        </ul>
                        <!-- /.nav-second-level -->
                    </li>
                    <li th:class="${(#httpServletRequest.requestURL.toString().contains('strategy'))} ? 'active'">
                        <a href="/strategy"><i class="fa fa-gears fa-fw"></i> Strategies<span class="fa arrow"></span></a>
                        <ul class="nav nav-second-level">
                            <li>
                                <a href="#" data-th-href="@{/strategy/list}">Strategy List</a>
                            </li>
                            <li>
                                <a href="/strategy/settings">Strategy Settings</a>
                            </li>
                            <li>
                                <a href="/strategy/run">Strategy Run</a>
                            </li>
                            <li>
                                <a href="/strategy/account">Strategy Account</a>
                            </li>
                        </ul>
                        <!-- /.nav-second-level -->
                    </li>
                    <li th:class="${(#httpServletRequest.requestURL.toString().contains('details/details'))} ? 'active'">
                        <a href="/trade/details"><i class="fa fa-tachometer fa-fw"></i> Trade Details<span class="fa arrow"></span></a>
                        <ul class="nav nav-second-level">
                            <li>
                                <a href="#">Orders</a>
                            </li>
                            <li>
                                <a href="#">Fills</a>
                            </li>
                            <li>
                                <a href="#">Transactions</a>
                            </li>
                            <li>
                                <a href="#">Positions</a>
                            </li>
                            <li>
                                <a href="#">Position Details</a>
                            </li>
                        </ul>
                        <!-- /.nav-second-level -->
                    </li>
                    <li th:class="${(#httpServletRequest.requestURL.toString().contains('summaries'))} ? 'active'">
                        <a href="#"><i class="fa fa-bar-chart-o fa-fw"></i> Trade Summaries<span class="fa arrow"></span></a>
                        <ul class="nav nav-second-level">
                            <li>
                                <a href="#">Trade Summary</a>
                            </li>
                            <li>
                                <a href="#">Trade Details</a>
                            </li>
                        </ul>
                        <!-- /.nav-second-level -->
                    </li>
                </ul> <!-- /#side-menu -->
            </div>    <!-- /.sidebar-collapse -->
        </nav>        <!-- /.navbar-static-side -->

        <div id="page-wrapper">
            <div class="row">
                <div class="col-lg-12">
                    <h1 class="page-header">Blank</h1>
                </div>
                <!-- /.col-lg-12 -->
            </div>
            <!-- /.row -->
        </div>
        <!-- /#page-wrapper -->

    </div>
    <!-- /#wrapper -->

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

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

</body>
</html>
The changes to this page are in the left navigation bar. The pattern in the highlighted code above is repeated for each of the sections in the nav. In lines 89 - 91 there is a conditional block to select this section of the nav if the page including this fragment has a URL that matches the pattern in the conditional. This conditional will cause the "security" section of the nav to open if we are on one of the pages associated with users, roles, or permissions.  Also, the links in the nav sections have been updated to the working links for the strategy and user pages.  This Thymeleaf fragment as displayed in a browser is shown below.


2. Next we create the user list page. Notice the lines highlighted below.
<!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="#{user.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="#{user.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="#{user.list.id.label}">Id</th>
                            <th class="col-xs-3" data-th-text="#{user.list.user.label}">Username</th>
                            <th class="col-xs-2" data-th-text="#{user.list.pass.label}">Password</th>
                            <th class="col-xs-2" data-th-text="#{user.list.enabled.label}">Enabled</th>
                            <th class="col-xs-3" data-th-text="#{user.list.role.label}">Role</th>
                            <th class="col-xs-1" data-th-text="#{user.list.actions.label}">Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        <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.rolename}">ROLE_TEST1</td>
                            <td>
                                <div class="btn-group">
                                  <button class="btn btn-warning btn-xs dropdown-toggle" type="button" data-toggle="dropdown" data-th-text="#{user.list.actions.label}">
                                    Actions<span class="caret"></span>
                                  </button>
                                  <ul class="dropdown-menu">
                                    <li>
                                        <a href="#" data-th-href="@{/user/edit(id=${user.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="@{/user/delete(id=${user.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>user2</td>
                            <td>password2</td>
                            <td>true</td>
                            <td>ROLE_TEST2</td>
                            <td>
                                <div class="btn-group">
                                  <button class="btn btn-warning btn-xs dropdown-toggle" type="button" data-toggle="dropdown" 
                                        data-th-text="#{user.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>
                
                <br />
                
                <form class="form" action="#" data-th-action="@{/user/add}" data-th-object="${userDTO}" 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-3" data-th-text="#{user.list.user.label}">Username</th>
                                <th class="col-xs-2" data-th-text="#{user.list.pass.label}">Password</th>
                                <th class="col-xs-2" data-th-text="#{user.list.enabled.label}">Enabled</th>
                                <th class="col-xs-3" data-th-text="#{user.list.role.label}">Role</th>
                                <th class="col-xs-1" data-th-text="#{user.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="*{username}" placeholder="Username"></input></td>
                                <td><input class="form-control" type="text" data-th-field="*{password}" placeholder="Password"></input></td>
                                <td>
                                    <select class="form-control" th:field="*{enabled}">
                                      <option th:each="type : ${enabledOptions}" th:value="${type}" th:text="${type}">Dropdown</option>
                                    </select>
                                </td>
                                <td>
                                    <select class="form-control" th:field="*{roleId}">
                                      <option th:each="role : ${allRoles}" th:value="${role.id}" th:text="${role.rolename}">Dropdown</option>
                                    </select>
                                </td>
                                <td>
                                    <button type="submit" class="btn btn-primary" data-th-text="#{button.label.add}">Add User</button>
                                </td>
                            </tr>
                            <tr>
                                <td class="col-xs-1"></td>
                                <td class="col-xs-3 text-danger" data-th-if="${#fields.hasErrors('username')}" data-th-errors="*{username}">username error</td>
                                <td class="col-xs-2 text-danger" data-th-if="${#fields.hasErrors('password')}" data-th-errors="*{password}">password error</td>
                                <td class="col-xs-2 text-danger" data-th-if="${#fields.hasErrors('enabled')}" data-th-errors="*{enabled}">enabled error</td>
                                <td class="col-xs-3 text-danger" data-th-if="${#fields.hasErrors('roleId')}" data-th-errors="*{roleId}">role 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>
There are a number of lines highlighted above. We will go through each one:
  • The new style block in the head section controls the size of our new action dropdown menu and makes the dropdown fit better on the page.
  • The th-each loop is responsible for displaying each of the users in the system on their own row in table.  Notice that the object being displayed is a user from the "users" object bound to the model in the controller.  The "users" object is a list of User Entity objects...not DTO's.
  • Lines 82 - 100 contain the new action dropdown menu.  This menu contains links to the edit and delete functionality that prior to this change were separate buttons.
  • Line 139 is the first line of the "add" form.  Notice that the target object in the form is the User DTO that was bound to the model by the controller.
  • The two selelct blocks are the dropdown menus for a user to be assigned an "enabled" status and a role.  Note the use of our two @ModelAttributes bound by the controllers: "enabledOptions" and "allRoles".  Let's look at the role dropdown in a little more detail:
    • The target field of this dropdown is the roleId field on our DTO.  
    • The option section contains a th:each to loop through all of the roles in the "allRoles" list.
    • The value in the dropdown is the role id, but the role name is actually displayed.
    • When the user selects an item in the dropdown, the value of the item is assigned to the roleId field in the DTO.
  • Finally, at the bottom of the page we have two conditional display blocks.  One block is responsible for displaying messages sent from the controller associated with the attribute "error", and the other block is responsible for displaying messages sent from the controller that are associated with the attribute "message".

This Thymeleaf fragment as displayed in a browser is shown below.


3. We will now move on to the 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="#{user.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="#{user.edit.head.title}">Edit User</h4>
                <div class="col-sm-2"></div>
                <div class="col-sm-6">
                    <form class="form-horizontal" action="#" data-th-action="@{/user/edit}" data-th-object="${userDTO}" method="post">
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.user.label}">Username</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="*{username}" data-th-field="*{username}" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.pass.label}">User Password</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="*{password}" data-th-field="*{password}" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.enabled.label}">Enabled</label>
                            <div class="col-sm-7">
                                <select class="form-control" th:id="*{enabled}" th:field="*{enabled}">
                                  <option th:each="type : ${enabledOptions}" th:value="${type}" th:text="${type}">Dropdown</option>
                                </select>
                            </div>
                        </div>
                        <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">
                                <select class="form-control" th:id="*{roleId}" th:field="*{roleId}">
                                      <option th:each="role : ${allRoles}" th:value="${role.id}" th:text="${role.rolename}">Dropdown</option>
                                </select>
                            </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('username')}" data-th-errors="*{username}">type error</p> 
                                <p class="text-danger" data-th-if="${#fields.hasErrors('password')}" data-th-errors="*{password}">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>
You will notice in the code above several highlighted lines.
  • The first highlighted line shows that the object we are using in this form is the User DTO object. We could potentially be passing back role changes and these need to be represented by a roleId variable rather than a Role object. 
  • The first select block looks similar to the select blocks in the add form on the list page. 
    • In this code, we use th:id="*{enabled}" to indicate which "enabled" value should be pre-selected from the "enabledOptions" list (a ModelAttribute from the controller) on page load.  This represents the current value in the User DTO used to populate this page.
    • On submit, the value selected is assigned to th:field="*{enabled}", the "enabled" field in the User DTO.
  • The second select block is associated with a user role, and looks similar to both the add form and the prior select block. A couple of points to note:
    • The "select" block contains source and target variables :"roleId" from the User DTO.
    • The "option" block uses the array of Role objects (contained in the ModelAttribute "allRoles") for the "value" and "text" variable values.

A screenshot of the Thymleaf fragement displayed in a browser is shown below:


4. Lastly, we have the user delete page. This page displays our User Entity object prior to deletion, and gives the end user the opportunity to confirm that this user should be deleted from the system. The username, role, etc are displayed in disabled form fields.
<!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="#{user.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="#{user.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="#{user.list.id.label}">User Id</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${user.id}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.user.label}">Username</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${user.username}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.pass.label}">User Password</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${user.password}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-5 control-label" data-th-text="#{user.list.enabled.label}">Enabled</label>
                            <div class="col-sm-7">
                                <input type="text" class="form-control" data-th-value="${user.enabled}" disabled="disabled" ></input>
                            </div>
                        </div>
                        <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.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="@{/user/delete(id=${user.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="@{/user/delete(id=${user.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 user CRUD functionality. Here are a few screenshots of the running application.
The User List Page

Successfully Adding a User

The Action Menu on the Uer List Page

We now have working CRUD functionality for our User Entity.

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

Monday, April 28, 2014

Spring Thymeleaf CRUD User Maintenance - Part 2

This post builds on the code from part 1 of this series, Spring Thymeleaf CRUD User Maintenance - Part 1.  In this post, I had intended to create the controller class to handle CRUD operations on the User object, as well as the Thymeleaf pages.  Since there are so many interesting topics to cover in these two areas, I've decided to split this post into two parts.  We will create the associated Thymeleaf CRUD pages, updates to the menus, and updates to Twitter Bootstrap functionality (associated with alerts and actions menus) in part 3.


1. The controller for the User pages, and the Thymeleaf User pages themselves will use the message file for labels and messages. The updated message file is shown below.  Pay particular attention to the sections labeled:
  • "# USER THYMELEAF LABELS"
  • "# CONTROLLER MESSAGES"
# HOME PAGE TITLES
admin.page.title=Admin Home Page
trader.page.title=Trader Home Page
user.page.title=User Home Page
login.page.title=Login Page

# GENERIC BUTTON LABELS
button.label.update=Update
button.label.edit=Edit
button.label.add=Add
button.label.save=Save
button.label.delete=Delete
button.label.cancel=Cancel

button.action.stage=stage
button.action.save=save
button.action.delete=delete
button.action.cancel=cancel

# STRATEGY THYMELEAF LABELS
strategy.list.page.title=Strategy List
strategy.list.head.title=Strategy List
strategy.list.body.title=Strategy List
strategy.list.table.title=Strategy List
strategy.list.id.label=Id
strategy.list.type.label=Strategy Type
strategy.list.name.label=Strategy Name
strategy.list.actions.label=Action
strategy.list.add.title=Add

strategy.delete.page.title=Delete Strategy
strategy.delete.head.title=Delete Strategy

strategy.edit.page.title=Edit Strategy
strategy.edit.head.title=Edit Strategy

# USER THYMELEAF LABELS
user.list.page.title=User List
user.list.table.title=User List
user.list.id.label=Id
user.list.user.label=Username
user.list.pass.label=Password
user.list.role.label=Role
user.list.enabled.label=Enabled
user.list.actions.label=Action
user.list.add.title=Add User

user.edit.page.title=Edit User
user.edit.head.title=Edit User

user.delete.page.title=Delete User
user.delete.head.title=Delete User

# CONTROLLER MESSAGES
ctrl.message.error.duplicate=The {0} ''{1}'' already exists in the system.
ctrl.message.error.duplicate.strategy=The {0} of type ''{1}'' with name ''{2}'' already exists in the system.
ctrl.message.error.notfound=The {0} ''{1}'' does not exist in the system.
ctrl.message.success.add=The {0} ''{1}'' was successfully added to the system.
ctrl.message.success.update=The {0} ''{1}'' was successfully updated in the system.
ctrl.message.success.cancel={0} of {1} ''{2}'' was cancelled.
ctrl.message.success.delete=The {0} ''{1}'' was successfully deleted from the system.
Notice the controller messages section. The numbers in brackets represent strings in an Object array that is passed to the Spring MessageSource.getMessage() method.  We will look at this method in more detail when we review the controller code.

Also, to display single quotes in the message displayed to the end user, we use another single quote to escape the second single quote.  So, to display one single quote we include two single quotes in the message text in the property file.  Here are two articles that discuss escaping quotes in property files:


2. The controller for the User pages is shown below, with comments following this code.
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.DuplicateUserException;
import com.dtr.oas.exception.RoleNotFoundException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.Role;
import com.dtr.oas.model.User;
import com.dtr.oas.service.RoleService;
import com.dtr.oas.service.UserService;

@Controller
@RequestMapping(value = "/user")
@PreAuthorize("denyAll")
public class UserController {
    static Logger logger = LoggerFactory.getLogger(UserController.class);
    static String businessObject = "user"; //used in RedirectAttributes messages 

    @Autowired
    private RoleService roleService;

    @Autowired
    private UserService userService;

    @Autowired
    private MessageSource messageSource;

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

    @ModelAttribute("enabledOptions")
    @PreAuthorize("hasAnyRole('CTRL_USER_LIST_GET','CTRL_USER_EDIT_GET')")
    public boolean[] getEnabledOptions() {
        boolean[] array = new boolean[2];
        array[0] = true;
        array[1] = false;
        return array;
    }

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_USER_LIST_GET')")
    public String listUsers(Model model) {
        logger.info("IN: User/list-GET");

        List<User> users = userService.getUsers();
        model.addAttribute("users", users);

        // if there was an error in /add, we do not want to overwrite
        // the existing user object containing the errors.
        if (!model.containsAttribute("userDTO")) {
            logger.info("Adding UserDTO object to model");
            UserDTO userDTO = new UserDTO();
            model.addAttribute("userDTO", userDTO);
        }
        return "user-list";
    }

    @RequestMapping(value = "/add", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_USER_ADD_POST')")
    public String addUser(@Valid @ModelAttribute UserDTO userDTO,
            BindingResult result, RedirectAttributes redirectAttrs) {
        
        logger.info("IN: User/add-POST");

        if (result.hasErrors()) {
            logger.info("UserDTO add error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result);
            redirectAttrs.addFlashAttribute("userDTO", userDTO);
            return "redirect:/user/list";
        } else {
            User user = new User();

            try {
                user = getUser(userDTO);
                userService.addUser(user);
                String message = messageSource.getMessage("ctrl.message.success.add", new Object[] {businessObject, user.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/user/list";
            } catch (DuplicateUserException e) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", new Object[] {businessObject, userDTO.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/user/list";
            } catch (RoleNotFoundException rnf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"role", userDTO.getRoleId()}, Locale.US);
                result.rejectValue("roleId", "0", message);
                redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result);
                redirectAttrs.addFlashAttribute("userDTO", userDTO);
                return "redirect:/user/list";
            }
        }
    }

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

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

        try {
            if (!model.containsAttribute("userDTO")) {
                logger.info("Adding userDTO object to model");
                User user = userService.getUser(id);
                UserDTO userDTO = getUserDTO(user);
                logger.info("User/edit-GET:  " + userDTO.toString());
                model.addAttribute("userDTO", userDTO);
            }
            return "user-edit";
        } catch (UserNotFoundException e) {
            String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"user id", id}, Locale.US);
            model.addAttribute("error", message);
            return "redirect:/user/list";
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_USER_EDIT_POST')")
    public String editUser(@Valid @ModelAttribute UserDTO userDTO,
            BindingResult result, RedirectAttributes redirectAttrs,
            @RequestParam(value = "action", required = true) String action) {

        logger.info("IN: User/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, userDTO.getUsername()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
        } else if (result.hasErrors()) {
            logger.info("User-edit error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result);
            redirectAttrs.addFlashAttribute("userDTO", userDTO);
            return "redirect:/user/edit?id=" + userDTO.getId();
        } else if (action.equals(messageSource.getMessage("button.action.save",  null, Locale.US))) {
            logger.info("User/edit-POST:  " + userDTO.toString());
            try {
                User user = getUser(userDTO);
                userService.updateUser(user);
                String message = messageSource.getMessage("ctrl.message.success.update", new Object[] {businessObject, userDTO.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
            } catch (DuplicateUserException unf) {
                String message = messageSource.getMessage("ctrl.message.error.duplicate", new Object[] {businessObject, userDTO.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/user/list";
            } catch (UserNotFoundException unf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {businessObject, userDTO.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/user/list";
            } catch (RoleNotFoundException rnf) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"role", userDTO.getRoleId()}, Locale.US);
                redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/user/list";
            }
        }
        return "redirect:/user/list";
    }

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

        User user;
        try {
            user = userService.getUser(id);
        } catch (UserNotFoundException e) {
            String message = "User number [" + id + "] does not exist in the system";
            redirectAttrs.addFlashAttribute("error", message);
            return "redirect:/user/list";
        }

        logger.info("IN: User/delete-GET | id = " + id + " | phase = " + phase + " | " + user.toString());

        if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = messageSource.getMessage("ctrl.message.success.cancel", new Object[] {"Delete", businessObject, user.getUsername()}, Locale.US);
            redirectAttrs.addFlashAttribute("message", message);
            return "redirect:/user/list";
        } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) {
            logger.info("     adding user: " + user.toString());
            model.addAttribute("user", user);
            return "user-delete";
        } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) {
            try {
                userService.deleteUser(user.getId());
                String message = messageSource.getMessage("ctrl.message.success.delete", new Object[] {businessObject, user.getUsername()}, Locale.US);
                redirectAttrs.addFlashAttribute("message", message);
                return "redirect:/user/list";
            } catch (UserNotFoundException e) {
                String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"user id", id}, Locale.US);
               redirectAttrs.addFlashAttribute("error", message);
                return "redirect:/user/list";
           }
        }

        return "redirect:/user/list";
    }

    @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());
        userDTO.setRoleId(user.getRole().getId());
        return userDTO;
    }

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

        Role role = roleService.getRole(userDTO.getRoleId());
        user.setRole(role);

        user.setUsername(userDTO.getUsername());
        user.setPassword(userDTO.getPassword());
        user.setEnabled(userDTO.getEnabled());
        return user;
    }
}
There are several points that I will discuss below.  The associated lines are highlighted in the code above.
  • There is an instance variable named businessObject.  This string is used in the messages returned from the controller and this variable allows us to reuse messages in the messages_en.properties file across controllers.
  • The @Autowired MessageSource variable allows us to access messages in the messages_en.properties file, and is used throughout the controller.
  • All of the methods in the controller are annotated with @PreAuthorize.  These tags contain permission values for fine grained authorization.  We set up the infrastructure to use permissions rather than roles for authorization in the last series of posts.
  • There are two methods that are annotated with @ModelAttributes.  These methods will populate the response with lists used to populate dropdown lists on the User Thymeleaf pages.
    • Also notice the call to the roleService to get all of the roles in the system.
  • Some of the methods in the controller that are annotated with @RequestMapping get/set a User DTO rather than a User Entity.  We have to use this DTO object to pass roleIds to/from a form.  Unfortunately, we can't assign Role objects in a form...as far as I know, so we need to use a new object for this purpose.  The list page uses Entities for the list, but a DTO for the add section.  The edit page uses a DTO, and the delete page uses an Entity.  You will see Thymeleaf examples in the next post.
  • In the addUser() method you will notice the call to messageSource.getMessage(), that we touched on in bullet one in this post.  The parameters are:
    • The key from the messages_en.properties file.
    • An object array containing the arguments that will be substituted for the {#} placeholders in the messages in the messages_en.properties file.
    • The Locale.
  • I add the message as a flash attribute with the identifier of either "message" or "error".  These attributes will be displayed on the Thymeleaf pages in a dedicated message display section.
  • Finally, note the two helper methods at the bottom of the controller that convert from DTO to Entity, and from Entity to DTO.


3. The User DTO that we created contains the same validations contained in the User Entity.  I don't like the redundancy, but I couldn't find an elegant solution.  Also, you will notice that the DTO contains a roleId instance variable of type int, rather than the role instance variable of type Role contained in the Entity.  The DTO code is shown below.
package com.dtr.oas.controller;
import java.io.Serializable;
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 UserDTO implements Serializable {

    private static final long serialVersionUID = -1631797874369735181L;
    static Logger logger = LoggerFactory.getLogger(UserDTO.class);
    
    private int id;
    
    @NotNull(message = "{error.user.username.null}")
    @NotEmpty(message = "{error.user.username.empty}")
    @Size(max = 50, message = "{error.user.username.max}")
    private String username;

    @NotNull(message = "{error.user.password.null}")
    @NotEmpty(message = "{error.user.password.empty}")
    @Size(max = 50, message = "{error.user.password.max}")
    private String password;
    
    private boolean enabled;
    private int roleId;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public boolean getEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    public int getRoleId() {
        return roleId;
    }
    public void setRoleId(int roleId) {
        this.roleId = roleId;
    }
    
    @Override
    public String toString() {
        return String.format("%s(id=%d, username=%s, password=%s, enabled=%b, roleID=%d)", 
                this.getClass().getSimpleName(), 
                this.getId(), 
                this.getUsername(), 
                this.getPassword(), 
                this.getEnabled(),
                this.getRoleId());
    }

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

        if (o instanceof UserDTO) {
            final UserDTO other = (UserDTO) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getUsername(), other.getUsername())
                    && Objects.equal(getPassword(), other.getPassword())
                    && Objects.equal(getEnabled(), other.getEnabled())
                    && Objects.equal(getRoleId(), other.getRoleId());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId(), getUsername(), getPassword(), getEnabled(), getRoleId());
    }
}

In the next post we will create the Thymeleaf CRUD pages, update the menus, and update the Twitter Bootstrap functionality (associated with alerts and actions menus).

Saturday, April 26, 2014

Spring Thymeleaf CRUD User Maintenance - Part 1

In this series of posts we will go back to coding the infrastructure around the option trading system.  We will build on the code from the last series, Spring Security 3.2 - Users, Roles, Permissions, and add CRUD functionality to the User entity.  While we are at it, we will also refactor some of the CRUD code for the Strategy entity.

1. Let's start by updating the security configuration.  We will be adding a controller to handle requests for user information and will have this controller handle URLs that match "/user".  The security configuration will need to be updated to allow access to this path as shown 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")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedExceptionHandler);
    }
}

2. Now we will move down to the entity layer. After a bit of thought, I've decided to change the User entity to have a one-to-one mapping to Role, rather than a one-to-many relationship.  This will allow a user to have only one role (rather than many), but fine grained permissions associated with this role.  The changes to the user object are highlighted below.
package com.dtr.oas.model;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
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 org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.google.common.base.Objects;

@Entity  
@Table(name="USERS")
public class User extends BaseEntity implements UserDetails {

    private static final long serialVersionUID = 6311364761937265306L;
    static Logger logger = LoggerFactory.getLogger(User.class);
    
    @NotNull(message = "{error.user.username.null}")
    @NotEmpty(message = "{error.user.username.empty}")
    @Size(max = 50, message = "{error.user.username.max}")
    @Column(name = "username", length = 50)
    private String username;

    @NotNull(message = "{error.user.password.null}")
    @NotEmpty(message = "{error.user.password.empty}")
    @Size(max = 50, message = "{error.user.password.max}")
    @Column(name = "password", length = 50)
    private String password;
    
    @Column(name = "enabled")
    private boolean enabled;
    
    @OneToOne(fetch = FetchType.EAGER)  
    @JoinTable(name = "user_roles",  
        joinColumns        = {@JoinColumn(name = "user_id", referencedColumnName = "id")},  
        inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}  
    )  
    private Role role;
    
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    @Override
    public String toString() {
        return String.format("%s(id=%d, username=%s, password=%s, enabled=%b)", 
                this.getClass().getSimpleName(), 
                this.getId(), 
                this.getUsername(), 
                this.getPassword(), 
                this.getEnabled());
    }

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

        if (o instanceof User) {
            final User other = (User) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getUsername(), other.getUsername())
                    && Objects.equal(getPassword(), other.getPassword())
                    && Objects.equal(getEnabled(), other.getEnabled());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId(), getUsername(), getPassword(), getEnabled());
    }

    @Transient
    public Set<Permission> getPermissions() {
        Set<Permission> perms = new HashSet<Permission>();
        perms.addAll(role.getPermissions()); 
        return perms;
    }

    @Override
    @Transient
    public Collection<GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        authorities.add(getRole());
        authorities.addAll(getPermissions());
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        //return true = account is valid / not expired
        return true; 
    }

    @Override
    public boolean isAccountNonLocked() {
        //return true = account is not locked
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        //return true = password is valid / not expired
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.getEnabled();
    }
}

3. At the DAO layer there are some changes to the update method. We want to allow a username to be changed in the application, but we need to make sure that usernames are not duplicated. In order to accommodate this functionality at the DAO layer, the update method needs to be changed. This is also true of our other DAOs. The modified method signature is highlighted in both the interface and the implementation below.
package com.dtr.oas.dao;
import java.util.List;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

public interface UserDAO {

    public void addUser(User user) throws DuplicateUserException;

    public User getUser(int userId) throws UserNotFoundException;
    
    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();

}
package com.dtr.oas.dao;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

@Repository
public class UserDAOImpl implements UserDAO {
    static Logger logger = LoggerFactory.getLogger(UserDAOImpl.class);

    @Autowired
    private SessionFactory sessionFactory;

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @Override
    public void addUser(User user) throws DuplicateUserException {
        logger.debug("UserDAOImpl.addUser() - [" + user.getUsername() + "]");
        try {
            User userCheck = getUser(user.getUsername());
            String message = "The user [" + userCheck.getUsername() + "] already exists";
            throw new DuplicateUserException(message);
        } catch (UserNotFoundException e) { 
            getCurrentSession().save(user);
        }
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + userId + "]");
        User userObject = (User) getCurrentSession().get(User.class, userId);
        
        if (userObject == null) {
            throw new UserNotFoundException("User id [" + userId + "] not found");
        } else {
            return userObject;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public User getUser(String usersName) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + usersName + "]");
        Query query = getCurrentSession().createQuery("from User where username = :usersName ");
        query.setString("usersName", usersName);
        
        logger.debug(query.toString());
        if (query.list().size() == 0 ) {
            throw new UserNotFoundException("User [" + usersName + "] not found");
        } else {
            logger.debug("User List Size: " + query.list().size());
            List<User> list = (List<User>)query.list();
            User userObject = (User) list.get(0);

            return userObject;
        }
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException {
        User userCheck = getUser(user.getId());
        
        if (userCheck.getId() == user.getId()) {
            userCheck.setEnabled(user.getEnabled());
            userCheck.setPassword(user.getPassword());
            userCheck.setUsername(user.getUsername());
            userCheck.setRole(user.getRole());
            getCurrentSession().update(userCheck);
        } else {
            String message = "The user [" + userCheck.getUsername() + "] already exists";
            throw new DuplicateUserException(message);
        }
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        User user = getUser(userId);
        if (user != null) {
            getCurrentSession().delete(user);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<User> getUsers() {
        return getCurrentSession().createQuery("from User").list();
    }
}
You will notice that in the update method, I am getting a User instance (userCheck) from the current hibernate session and performing not null and id checks. I then assign the values from the method argument (user) to this new instance (userCheck), prior to calling update with the userCheck object. Until I performed this mapping and used this update approach, I received the hibernate error: "A different object with the same identifier value was already associated with the session"


4. The service layer classes require method signature modifications for the update methods. The updated interface and implementation classes are shown below, with the changes highlighted.
package com.dtr.oas.service;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

public interface UserService extends UserDetailsService {

    public void addUser(User user) throws DuplicateUserException;

    public User getUser(int userId) throws UserNotFoundException;

    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();
}
package com.dtr.oas.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.dtr.oas.dao.UserDAO;
import com.dtr.oas.exception.DuplicateUserException;
import com.dtr.oas.exception.UserNotFoundException;
import com.dtr.oas.model.User;

@Service
@Transactional
public class UserServiceImpl implements UserService {
    static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    
    @Autowired
    private UserDAO userDAO;

    @Override
    public void addUser(User user) throws DuplicateUserException {
        userDAO.addUser(user);
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        return userDAO.getUser(userId);
    }

    @Override
    public User getUser(String username) throws UserNotFoundException {
        return userDAO.getUser(username);
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException, DuplicateUserException {
        userDAO.updateUser(user);
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        userDAO.deleteUser(userId);
    }

    @Override
    public List<User> getUsers() {
        return userDAO.getUsers();
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            return getUser(username);
        } catch (UserNotFoundException e) {
            throw new UsernameNotFoundException(e.getMessage());
        }
    }
}

In the next post we will create a controller class, a DTO for the User entity, and create the Thymeleaf pages (with some Bootstrap updates as well).

Friday, April 18, 2014

Spring Security 3.2 - Users, Roles, Permissions - Part 5

This post builds on the code from part 4 of this series,Spring Security 3.2 - Users, Roles, Permissions - Part 4.  In this post, we will create a new package and move the exception/exception handling classes to this package in order to more easily accommodate unit testing with JUnit.  The revised directory structure and refactored class names are shown in the image below.

1. We will first create a new package, com.dtr.oas.exception.  We will then move all of the exception classes from com.dtr.oas.dao to this new package as shown in the image above.


2. Next, we will move the AccessDeniedExceptionHandler from com.dtr.oas.controller to com.dtr.oas.exception as shown in the image above.


3. The import statement in the SecurityConfig file should be updated to refer to the AccessDeniedExceptionHandler in the new package.


4. Now we will rename com.dtr.oas.DatabaseConfig to com.dtr.oas.RootConfig.


5. The import statement in the Initializer file should be updated to refer to the RootConfig file rather than the DatabaseConfig file.


6. Lastly, the component scan entry in the RootConfig file should be updated to the following.
...
import com.dtr.oas.exception.AccessDeniedExceptionHandler;

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.dtr.oas.dao", "com.dtr.oas.model", "com.dtr.oas.service", "com.dtr.oas.util"})
@PropertySource("classpath:database.properties")
public class RootConfig {

private static final String PROPERTY_NAME_DATABASE_DRIVER   = "db.driver";
...
In the WebAppConfig.java file, all packages under "com.dtr.oas" are scanned, while in the RootConfig.java file, all packages except the "com.dtr.oas.controller" package are scanned.  The RootConfig file is used to prepare the environment for JUnit and we are not running any controller tests at this time (so we need to exclude controllers from this environment prep).  Much of this package and config reorg was to enable unit testing of the service and DTO layers.


7. At this time we should now be able to run the application and test the security configuration.  The HttpSecurity line in the SecurityConfig will keep all users not in the ADMIN role from being able to access the Strategy pages.  To test that the method level authorization is functional on the Strategy pages, we can check the database to review the pemissionname and associated id.
In the screen shot above, we can see that ID number 3 corresponds to the permissionname "CTRL_STRATEGY_EDIT_GET".  This is the permission that we want to reassign.

Now we can modify the ROLE_ID field in role_permissions table as shown in the screenshot below.
Rather than having the permission id number 3 associated with the ADMIN role, we will reassign this permission to the TRADER role.

To confirm this authorization change, log in as the admin user, go to the strategy list page and try to edit a strategy.  If the authorization update is working, the admin user should receive our custom 403 page when they try to edit a strategy.  All other strategy functionality should be working for the admin user.  After the confirmation, change the role id value back to ADMIN for our edit permission.

In the next posts, we will add CRUD functionality to our User, Role, and Permission entities.

Code at GitHub: https://github.com/dtr-trading/spring-ex14-auth

Spring Security 3.2 - Users, Roles, Permissions - Part 4

This post builds on the code from part 3 of this series,Spring Security 3.2 - Users, Roles, Permissions - Part 3.  In this post, we will add/update the controller layer to utilize the new authorization scheme. There are only two controller classes to update.

1. We will start by updating the link controller in the areas highlighted below.  Rather than getting the single role from the User object, we now need to get all of the authorities (roles and permissions) from the User object.  The method getAuthorities() highlighted at the bottom of the controller performs this function.  We then iterate through the authorities to determine which role is associated with this user.  As before, the uesr's role dictates the home-page for that user.
package com.dtr.oas.controller;
import java.util.Collection;
import java.util.HashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.dtr.oas.model.User;

@Controller
public class LinkController {
    static Logger logger = LoggerFactory.getLogger(LinkController.class);

    @RequestMapping(value = "/")
    public String mainPage() {
        Collection<GrantedAuthority> authorities = getAuthorities();
        String rolename;
        
        for (GrantedAuthority authority : authorities) {
            rolename = authority.getAuthority();
            
            if (rolename.equals("ROLE_ADMIN")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-admin";
            }
            if (rolename.equals("ROLE_TRADER")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-trader";
            }
            if (rolename.equals("ROLE_USER")) {
                logger.debug("Directing to home page for: [" + rolename + "]");
                return "home-user";
            }
        }
        
        logger.error("Role not found - directing to home page for ROLE_USER");
        return "home-user";
    }

    @RequestMapping(value = "/index")
    public String indexPage() {
        return "redirect:/";
    }
    
    private Collection<GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof User) {
            authorities = ((User) principal).getAuthorities();
        } else {
            logger.error("Principal is not an instance of com.dtr.oas.model.User");
        }
        return authorities;
    }

}

2. Next we will update the strategy controller to user our new authentication scheme. The changed lines are highlighted in blue. The first modification is to deny access to all methods in the class using the "denyAll" declaration.  Then, we selectively turn on access to each of the methods using the permission strings that we loaded into the permissions table in the database.  For example, the addingStrategy() method can only be invoked if the user has a role that has the permission "CTRL_STRATEGY_ADD_POST".
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.model.Strategy;
import com.dtr.oas.service.StrategyService;

@Controller
@RequestMapping(value = "/strategy")
@PreAuthorize("denyAll")
public class StrategyController {
    static Logger logger = LoggerFactory.getLogger(StrategyController.class);

    @Autowired
    private StrategyService strategyService;

    @Autowired
    private MessageSource messageSource;

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_STRATEGY_LIST_GET')")
    public String listOfStrategies(Model model) {
        logger.info("IN: Strategy/list-GET");

        List<Strategy> strategies = strategyService.getStrategies();
        model.addAttribute("strategies", strategies);

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

        logger.info("IN: Strategy/add-POST");

        if (result.hasErrors()) {
            logger.info("Strategy-add error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.strategy", result);
            redirectAttrs.addFlashAttribute("strategy", strategy);
            return "redirect:/strategy/list";
        } else {
            strategyService.addStrategy(strategy);
            String message = "Strategy " + strategy.getId() + " was successfully added";
            redirectAttrs.addFlashAttribute("message", message);
            return "redirect:/strategy/list";
        }
    }

    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    @PreAuthorize("hasRole('CTRL_STRATEGY_EDIT_GET')")
    public String editStrategyPage(@RequestParam(value = "id", required = true) 
            Integer id, Model model) {
        
        logger.info("IN: Strategy/edit-GET:  ID to query = " + id);

        if (!model.containsAttribute("strategy")) {
            logger.info("Adding Strategy object to model");
            Strategy strategy = strategyService.getStrategy(id);
            logger.info("Strategy/edit-GET:  " + strategy.toString());
            model.addAttribute("strategy", strategy);
        }

        return "strategy-edit";
    }
        
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    @PreAuthorize("hasRole('CTRL_STRATEGY_EDIT_POST')")
    public String editingStrategy(@Valid @ModelAttribute Strategy strategy,
            BindingResult result, RedirectAttributes redirectAttrs,
            @RequestParam(value = "action", required = true) String action) {

        logger.info("IN: Strategy/edit-POST: " + action);

        if (action.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = "Strategy " + strategy.getId() + " edit cancelled";
            redirectAttrs.addFlashAttribute("message", message);
        } else if (result.hasErrors()) {
            logger.info("Strategy-edit error: " + result.toString());
            redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.strategy", result);
            redirectAttrs.addFlashAttribute("strategy", strategy);
            return "redirect:/strategy/edit?id=" + strategy.getId();
        } else if (action.equals(messageSource.getMessage("button.action.save",  null, Locale.US))) {
            logger.info("Strategy/edit-POST:  " + strategy.toString());
            strategyService.updateStrategy(strategy);
            String message = "Strategy " + strategy.getId() + " was successfully edited";
            redirectAttrs.addFlashAttribute("message", message);
        }

        return "redirect:/strategy/list";
    }

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

        Strategy strategy = strategyService.getStrategy(id);
        logger.info("IN: Strategy/delete-GET | id = " + id + " | phase = " + phase + " | " + strategy.toString());

        if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) {
            String message = "Strategy delete was cancelled.";
            model.addAttribute("message", message);
            return "redirect:/strategy/list";
        } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) {
            String message = "Strategy " + strategy.getId() + " queued for display.";
            model.addAttribute("strategy", strategy);
            model.addAttribute("message", message);
            return "strategy-delete";
        } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) {
            strategyService.deleteStrategy(id);
            String message = "Strategy " + strategy.getId() + " was successfully deleted";
            model.addAttribute("message", message);
            return "redirect:/strategy/list";
        }

        return "redirect:/strategy/list";
    }
}

In the next post, we will make some configuration changes to accommodate unit testing, and then show the results.