CustomMenu

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).

Thursday, April 24, 2014

Thoughts on Options Strategy Backtests

This post will be a bit of a diversion from the code examples.  I will discuss some of the ideas behind my backtester and the reasons why I will start by showing backtests of iron condors.  Future posts will initially show automated backtesting results for variations of iron condors.


Short Iron Condor Overview
In case you're not familiar, an iron condor is composed of put and call credit spreads.  It is common to have the same number of shorts and longs in each spread.  Here is a typical iron condor structure:
  • Short 10 (-10),  RUT  Jul-2014  970 puts 
  • Long 10 (+10),  RUT  Jul-2014  950 puts 
  • Short 10 (-10),  RUT  Jul-2014  1270 calls
  • Long 10 (+10),  RUT  Jul-2014  1290 calls

From a risk graph perspective, an iron condor will look like the image below.  I am including this image only to show the risk graph structure of this strategy, and not to recommend this as a trade.


If you need more information, there are many articles about iron condors on the internet.  To save you a bit of Googling, here are two articles to get you started:


Why Iron Condors
I'm going to assume at this point that you have some knowledge of option's Greeks.  If not, the two articles below should give you enough background to understand this section:
If you are selling options either alone or as part of a strategy (like an iron condor), time decay (theta decay) on an option's value is your friend.  Assuming all inputs to Black-Scholes are constant (they never are in real life!), an option's value will decrease with time.  In other words, an option's extrinsic value will approach zero as it's days to expiration (DTE) approach zero.  This allows you to sell an option at one price and theoretically buy it back at a lower price, netting the difference as profit.

Theoretical theta decay does not occur at the same rate for all option strikes though.  Out-of-the-money (OTM) options decay differently than at-the-money (ATM) options.  There are many charts showing ATM theta decay of options, but very few showing OTM theta decay.  This first graph below shows the theoretical theta decay of an SPX ATM call and an SPX ATM put.  You can see that most of the ATM time decay happens in the last 30 DTE.



The next graph shows the theoretical theta decay for a 10 delta SPX OTM call and a 10 delta SPX OTM put.  You can see that most of the OTM theta decay happens prior to 30 DTE.



I originally became interested in iron condors because of the theoretical theta decay of OTM options.  There are trade offs with all options in terms of delta, vega, and gamma and OTM options are particularly exposed to vega risk.  When you analyze the theoretical theta decay of low delta (high prob) iron condors, the decay looks very similar to the decay of OTM options (as you would expect).  The graph below shows the theoretical decay for iron condors with different delta short strikes and different wing widths.



Automated Backtesting
When I originally designed the first version of my option strategy backtester (OSB), I was trying to compare the performance of different strategies and variations of those strategies.  I started by building an iron condor strategy and added other components as I built out more strategies.  With these posts, I will start by showing the results for basic iron condors and build from there (with uneven wings, different quantities of spreads, hedges, etc.).

Live trading and backtesting are very different and I never looked at my backtest results as "real".  Instead, I was interested in comparing the relative performance of strategies to their variations.  Please do not look at the results in future posts as "real" trades, but instead compare strategies to each other in terms of standard trade metrics.  Ideally we will learn which trade types perform better in different market environments and start to identify when a particular strategy is performing outside of it's norms.  Because of this view, slippage and commissions will not be included in the backtests.

In my live trading, I allocate a fixed amount of capital monthly to trades, and do not increase the size of my trades with the profits.  When I show results in future posts, I will show them from this perspective.  I will not compound the returns (no CAGR), instead I will simply add the profits and losses.  In my live trading it is not uncommon to have multiple expiration months of trades "on" at the same time.  The backtest results that I will show will take this same approach of having overlapping trades, with two or three trades on at the same time.

Finally, my tests are run with end-of-day (EOD) options data.  I have several years of 15-minute data and found that the relative results of a strategy did not change much if I used EOD data or 15-minute data.  Backtests are much quicker with EOD data rather than 15-minute data.

Hopefully this post answers some of your questions so that we can focus on test results in the future posts.

Saturday, April 19, 2014

Automated Options Trading Backtesting

I started this blog in 2013 as a way to capture my ideas for building an automated option trading system (Option Algo System - OAS).  At that time, I had already built version 1 of an option trading backtesting system (Option Strategy Backtester - OSB) and I was researching how to best integrate my backtesting system (written in Java) with an auto trading system.

Needless to say, as I worked towards the auto trading goal I had to refactor/redesign my backtesting system to utilize its strategies in live trading.  I'm now on version 3 of the backtesting system (OSB) and the architecture of the software has changed significantly.  Version 4 will be a radical architecture change as the system moves from strategies defined in property files to strategies defined in a database, and updated in the user interface you see taking shape on this blog.  Also the output will move from being auto-generated in CSV files to output also being stored in a database (MySQL).

The data architecture of the trading system (OAS) is still similar to the diagram below.

I've left a few details (columns) off of some of the tables in order to show all of the tables and their relationships in the diagram.  For example, security_option will have many more columns.  The equivalent table in the backtesting system (OSB) currently has 22 columns.  That means for every option, there are 22 attributes.  The two tables outlined in blue have been built in the auto trading system (OAS).  In order to have the auto trading system (OAS) be minimally integrated with the backtesting system (OSB) the tables in red will need to be built along with the associated Java code.  The tables not outlined, along with the associated Java code, will be built next.  The last tables/code to be built are those outlined in green.

Until the first step of the integration is complete (tables in red), I will post some backtesting results from OSB version 3.  I will shoot for weekly posts of backtesting results interspersed with OAS development posts.  If you're not interested in the system development, but you're interested in options trading, I will start to post for you soon.

My first post with backtesting results will be next week (after I download the options data through this week for RUT, SPX, and NDX).  This first backtesting post will cover a standard iron condor using monthly options exprirations, with end-of-day (EOD) option data for the three vehicles (RUT, SPX, NDX).  I will show the results for three or four different short strike deltas iron condors with a fixed days-to-expiration (DTE) trade start.  More details in the actual post ...