CustomMenu

Wednesday, February 26, 2014

Spring MVC 4 - Bootstrap Navigation and Thymeleaf Fragments

In this post, I am going to use Thymeleaf fragments and a Bootstrap based theme to add horizontal and vertical navbars to the example.  In addition, I've cleaned up the controller code a bit to only use Model objects where needed.

1. The cleaned up controller is shown below.
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.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")
public class StrategyController {
    static Logger logger = LoggerFactory.getLogger(StrategyController.class);

    @Autowired
    private StrategyService strategyService;

    @Autowired
    private MessageSource messageSource;

    @RequestMapping(value = {"/", "/list"}, method = RequestMethod.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)
    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)
    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)
    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)
    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";
    }
}

2. Next, we are going to use the SB-Admin theme based on Bootstrap.  Download the theme, sb-admin-v2.zip, at: http://startbootstrap.com/sb-admin-v2


3. After you unzip the theme, review the file "blank.html" highlighted in the image below.  We will need to recreate the sections of this page in our four Thymeleaf html pages.  Also, notice the directory structure that we will need to reproduce in our Eclipse project.




4. The original directory/file structure under webapp/resources looked like the first image below.  We will need to copy the files from the sb-admin-v2 template to create the directory/file structure shown in the second image.
Original Directory Structure

New Directory Structure
 You will notice that we only copied over the files from the template that we needed to recreate the navbars.  The files and directories include:
  • resources/css/sb-admin.css
  • resources/font-awsome/
  • resources/js/sb-admin.css
  • resources/js/plugins/metisMenu/

5. Next, copy the file "blank.html" to a new directory named "fragments" under our WEB-INF/views directory.  Rename this file to "sb-admin.html".  We are going to convert this file to a Thymeleaf file, containing fragments that we can reuse in our other four Thymeleaf HTML files.  After removing all of the dummy data in the original file, and using Thymeleaf tags, we now have the file 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>
    <!-- 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="#">Option Algo System</a>
            </div>
            <!-- /.navbar-header -->

            <ul class="nav navbar-top-links navbar-right">
                    <li><a href="/"            >Home</a></li>
                    <li><a href="/trading"     >Trading</a></li>
                    <li><a href="/backtesting" >Backtesting</a></li>
                    <li><a href="/admin"       >Admin</a></li>
                    <li><a href="#"><font color="#049cbd">Dave</font></a></li>
                    <li><a href="#"><button type="button" class="btn btn-default btn-xs btn-primary">
                                        <span class="glyphicon glyphicon-off"></span></button>
                       </a></li>
            </ul>  <!-- /.navbar-top-links -->
        </nav>     <!-- /.navbar-static-top -->

        <nav data-th-fragment="vert-nav-admin" class="navbar-default navbar-static-side" >
            <div class="sidebar-collapse">
                <ul class="nav" id="side-menu">
                    <li>
                        <a href="/roles"><i class="fa fa-lock fa-fw"></i> Roles</a>
                    </li>
                    <li>
                        <a href="/users"><i class="fa fa-user fa-fw"></i> Users</a>
                    </li>
                    <li>
                        <a href="/accounts"><i class="fa fa-tasks fa-fw"></i> Accounts</a>
                    </li>
                    <li>
                        <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="/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>
                        <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 class="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>
There are a few points to notice in the file above:
  • With the excpetiion of including new css files, the head section of the file looks exactly like our Thymeleaf HTML files.
  • The first tag inside the body tag is a div tag with an id="wrapper.  This will need to be the first tag inside the body of our four HTML files.
  • The first nav block is our horizontal navbar.  I've modifed it slightly to look more like the header that will be in the trading application.  This first nav block is also identified as a Thymeleaf fragment with the tag data-th-fragment="top-nav".
  • The second nav block, at the same level as the prior bullet, is the vertical navbar.  This has also been modified to list trading related functions, but not necessarily the final layout.  This nav block is also identified as a Thymeleaf fragment with the tag data-th-fragment="vert-nav-admin".
  • The last block is the placeholder for our actual pages, and starts with the tag div id="page-wrapper".
  • At the end of the file we again include the javascript files required for Bootstrap and the SB-Admin theme.
  • Notice that all of the source paths in this file have three levels of ../ rather than two levels.  This is because our template is included one level lower than our view files.  These references will be two levels deep in our view files.

6. If you open this template file in a browser, it should look like the one below.  It is tricky to get all of the tags and paths correct, so this might take a bit of troubleshooting.

7. Now, in each of the four HTML files (home.html, strategy-delete.html. strategy-edit.html, strategy-list.html), we will need to update the header and footer, and include fragment references to our nav bars.  The home.html file should look like the file below.  The other updated files are in the GitHub repository for this post.
<!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="#{strategy.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-admin"></div>

    <div id="page-wrapper">
        <div class="row">
            <div class="col-lg-12">
            
    <h4 class="page-header" data-th-text="#{strategy.list.table.title}">Configured Strategies</h4>
    <div class="table responsive">
    <table class="table table-striped table-bordered table-hover">
     <thead>
      <tr>
       <th class="col-sm-1" data-th-text="#{strategy.list.id.label}">Id</th>
       <th class="col-sm-4" data-th-text="#{strategy.list.type.label}">Strategy Type</th>
       <th class="col-sm-4" data-th-text="#{strategy.list.name.label}">Strategy Name</th>
       <th class="col-sm-2" data-th-text="#{strategy.list.actions.label}">Action</th>
      </tr>
     </thead>
     <tbody>
      <tr data-th-each="strategy : ${strategies}">
       <td data-th-text="${strategy.id}">1</td>
       <td data-th-text="${strategy.type}">Iron Butterfly</td>
       <td data-th-text="${strategy.name}">Triple Butter</td>
       <td style="text-align: center;">
        <a href="#" data-th-href="@{/strategy/edit(id=${strategy.id})}">
         <button type="button" class="btn btn-default btn-xs">
          <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;Edit
         </button></a> &nbsp; 
        <a href="#" data-th-href="@{/strategy/delete(id=${strategy.id},phase=stage)}">
         <button type="button" class="btn btn-default btn-xs">
          <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;Delete
         </button></a>
       </td>
      </tr>
      <tr data-th-remove="all">
       <td>2</td>
       <td>Iron Condor</td>
       <td>High Prob Hedged</td>
       <td style="text-align: center;">
        <a href="#">
         <button type="button" class="btn btn-default btn-xs">
          <span class="glyphicon glyphicon-pencil"></span>&nbsp;&nbsp;Edit
         </button></a>&nbsp; 
        <a href="#">
         <button type="button" class="btn btn-default btn-xs">
          <span class="glyphicon glyphicon-trash"></span>&nbsp;&nbsp;Delete
         </button></a>
       </td>
      </tr>
     </tbody>
    </table>
    </div>
    
    <br />
    
    <form class="form" action="#" data-th-action="@{/strategy/add}" data-th-object="${strategy}" method="post">
    <div class="table responsive">
     <table class="no-border-on-me table ">
      <thead>
       <tr>
        <th class="col-sm-1"></th>
        <th class="col-sm-4" data-th-text="#{strategy.list.type.label}">Strategy Type</th>
        <th class="col-sm-4" data-th-text="#{strategy.list.name.label}">Strategy Name</th>
        <th class="col-sm-2" data-th-text="#{strategy.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="*{type}" placeholder="Type"></input></td>
        <td><input class="form-control" type="text" data-th-field="*{name}" placeholder="Name"></input></td>
        <td>
         <button type="submit" class="btn btn-primary" data-th-text="#{button.label.add.strategy}">Add Strategy</button>
        </td>
       </tr>
       <tr>
        <td class="col-sm-1"></td>
        <td class="col-sm-4 text-danger" data-th-if="${#fields.hasErrors('type')}" data-th-errors="*{type}">type error</td>
        <td class="col-sm-4 text-danger" data-th-if="${#fields.hasErrors('name')}" data-th-errors="*{name}">name error</td>
        <td class="col-sm-2"></td>
       </tr>
      </tbody>
     </table>
    </div>
    </form>
    
            </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>
This files follows the same structure as the "sb-admin.html" structure, the same div tags outlining the main body of the page.  At the top of this page, the navbars from the "sb-admin.html" file are included with the th:replace tags.


8. Screenshots of the home.html and strategy-list.html files are shown below.



In future posts we will look at integrating Spring Security, JPA/connection pooling, and unit testing.  After these steps are complete, we should be ready to start building out the trading system core.

Code at GitHub: https://github.com/dtr-trading/spring-ex07-navigation

No comments:

Post a Comment