CustomMenu

Thursday, February 27, 2014

Spring MVC 4 & Spring Security 3.2 - Part 1

In this series of posts, will add Spring Security to our application.  We will start by adding very simple authentication functionality driven by usernames and passwords contained in configuration files.  Once that functionality is operating correctly, we will move this authentication information into MySQL.  The last step will be to add role authorization to our application.

In part 1, we will follow along with the Spring Security presentation by Rob Winch/Spring.io:

1. We will first create two new configuration files, and modify our web application config file.
package com.dtr.oas.config;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityInitializer extends  AbstractSecurityWebApplicationInitializer {
}
package com.dtr.oas.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
        //.and().httpBasic();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().
        withUser("user").password("password").roles("USER").and().
        withUser("trader").password("password").roles("USER","TRADER").and().
        withUser("admin").password("password").roles("USER", "TRADER", "ADMIN");
    }
}
package com.dtr.oas.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("com.dtr.oas")
public class WebAppConfig extends WebMvcConfigurerAdapter {
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }

    // Maps resources path to webapp/resources
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }

    @Bean
    public UrlBasedViewResolver setupViewResolver() {
        UrlBasedViewResolver resolver = new UrlBasedViewResolver();
        resolver.setPrefix("/WEB-INF/pages/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);
        return resolver;
    }

    // Provides internationalization of messages
    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("messages");
        return source;
    }

}
When you are finished, your configuration directory structure should look like the image below.


2. With these three files in place (containing sample users and roles), our simple security is almost complete.  You will notice in the WebAppConfig.java file we added configuration for a controller to direct requests for "/login" to a custom login page.  This login page is called login.html and will need to be in the root of our WEB-INF/views directory.  This is the same directory containing our other strategy HTML pages.  The login page is shown 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 data-th-text="#{login.page.title}">Login Page</title>
</head>

<body>

    <div class="container">
        <div class="row">
            <div class="col-md-4 col-md-offset-4">
                <div class="login-panel panel panel-default">
                    <div class="panel-body">
                        <form name="f" data-th-action="@{/login}" method="post">
                            <fieldset>
                    <legend>Please Login</legend>
                    <div data-th-if="${param.error}" class="alert alert-danger">
                        Invalid username and password.
                    </div>
                    <div data-th-if="${param.logout}" class="alert alert-success">
                        You have been logged out.
                    </div>
                                <div class="form-group">
                                    <label for="username">Username</label>
                                    <input class="form-control" placeholder="User ID" type="text" id="username" name="username"></input>
                                </div>
                                <div class="form-group">
                                    <label for="password">Password</label>
                                    <input class="form-control" placeholder="Password" type="password" id="password" name="password"></input>
                                </div>
                                <div class="form-actions">
                                    <button type="submit" class="btn btn-lg btn-success btn-block">Login</button>
                                </div>
                            </fieldset>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 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 head section of this file looks the same as our other Thymeleaf files, and the same goes for the script inclusion in the footer.  In between these sections is our login form using the HTTP-POST method, and Thymeleaf error reporting tags.


3. Our last step is to update our Thymeleaf horizontal navbar fragment to display the username and provide logout functionality.  The updated navbar fragment is shown below.
<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" data-th-with="currentUser=${#httpServletRequest.userPrincipal?.name}">
                    <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 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 -->

4. With these files in place, we can now attempt to login.  The first image below shows our login page as it is initially served up by Tomcat.  The second image shows the result of an incorrect password.



5. After successful login and navigation from the home page to the list page, you should see the screen in the image below.
The update to the navbar fragement, has given us a dropdown with the title containing the username of our logged in user.  The profile and settings links are stubs, but the logout button is functional.


6. After pressing the logout button, you should be taken back to the login screen, but with a logout message.


If you want more information on this topic, the following pages on the Spring Security site go over in a lot of detail, the steps above.

In the next post, we will move the user information from configuration files to a database.

Code at GitHub: https://github.com/dtr-trading/spring-ex08-security

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

Monday, February 24, 2014

Spring MVC 4 - Bootstrap and Validation

In this post we build on the example from the prior posts.  Here we add input validation by updating the strategy controller, strategy list/add page, strategy update/edit page, and the strategy entity.

After a bit of research, I've decided to remove the DTO layer for the CRUD operations.  It seems there is quite a bit of disagreement around DTOs, especially around the use of DTOs for the view layer, that exacly duplicate the fields and methods of an entity/model object. Here is the link to one of the articles about DTO/VO and their use relative to Domain/@Entity objects:

Controller vs Service vs private method on command object.

1. The first step in this example is to update the entity/model layer.  We will add some constraint and column annotations to the entity, and also implement the toString(), equals(), and hashCode() methods.  In addition we will move the id attribute to a common entity base class.  The two classes are shown below.
package com.dtr.oas.model;

import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import com.google.common.base.Objects;

@MappedSuperclass()
public abstract class BaseEntity {

 @Id
 @GeneratedValue
 private Integer id;

 public Integer getId() {
  return id;
 }

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

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

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

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

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

}
And now for the Strategy entity/model class:
package com.dtr.oas.model;

import java.io.Serializable;

import com.google.common.base.Objects;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotEmpty;

@Entity
@Table(name = "STRATEGY")
public class Strategy extends BaseEntity implements Serializable {

 private static final long serialVersionUID = 96285180113476324L;

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

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

 public String getType() {
  return type;
 }

 public void setType(String type) {
  this.type = type;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

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

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

  if (o instanceof Strategy) {
   final Strategy other = (Strategy) o;
   return Objects.equal(getId(), other.getId()) && 
       Objects.equal(getType(), other.getType()) && 
       Objects.equal(getName(), other.getName());
  }
  return false;
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(getId(), getType(), getName());
 }
}
In the Strategy class, you will notice that there are annotation tags added for each instance variable.  The @NotNull and @Size annotations are part of javax.validation, and the @NotEmpty annotation is part of the hibernate.validator.  With these annotations in place, we can call @Valid in our controller to run all of these validations against this Strategy entity.  I have also added the @Column annotation more as a reminder about the size of each column and to make sure the @Size values match the respective @Column length values.  BTW, here is an interesting comparison of approaches for overriding toString(), equals(), and hashCode():

http://www.halyph.com/2013/05/equals-hashcode-and-tostring-in-java.html


2. Next we will use the built in method for externalizing the validation error messages.  We will create a ValidationMessages.properties file in the main/resources directory in Eclipse.  This file contains simple key/value pairs.  The message keys defined in the Strategy class map to message keys in the properties file, which are then resolved to values (messages for display).  A good article on Spring message validation can be found here:

http://www.silverbaytech.com/2013/04/16/custom-messages-in-spring-validation/
error.strategy.type.null=Strategy type required
error.strategy.type.empty=Strategy type required
error.strategy.type.max=Type must be less than 20 characters

error.strategy.name.null=Strategy name required
error.strategy.name.empty=Strategy name required
error.strategy.name.max=Name must be less than 20 characters

3. The controller is next.  The controller will be updated to perform validation and pass error messages back to the Thymeleaf pages.  In addition we will add logging to the controller and update the controller methods to return Strings rather than ModelAndView objects.
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(! 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,  
                                 Model model, 
                                 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";
            model.addAttribute("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, 
                                        Model model, 
                                        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";
            model.addAttribute("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";
            model.addAttribute("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";
    }
}
At the top of the controller file you can see the slf4j logger definition.  In the list-GET method you will notice that this method now takes a Model parameter.  This is needed for displaying error messages from the add-POST method.  We have a conditional to make sure we do not add a new empty Strategy object to the model...in the situation where there is already a Strategy object present containing an error from the add-POST method.  Also, all of the methods in the controller now return a String rather than a ModelAndView object.

In the add-POST method there are now new method parameters and an @Valid annotation on the strategy to be added.  The BindingResult must immediately follow the validated object in the parameter sequence.  The Model must then follow the Binding Result, followed by the RedirectAttributes.  The BindingResult is checked for errors from the @Valid calls on the Strategy to be added.  If there are errors, the errors are added to the RedirectAttributes and a redirect back to list-GET occurs.  If there are no errors, then the Strategy is added and the user is redirected back to list-GET.

Edit-GET looks similar to list-GET, in that a Model object is now in the method parameter list.  If the Edit-GET method invocation occurs prior to the submit of an edit/update, then a new empty Strategy object is added to the Model, otherwise the Strategy object containing errors is left in place.

The edit-POST method is similar to the add-POST method.  The same method attributes are in place, but there is also an action request parameter that indicates whether there is a "cancel" or "save".  There is also an error check, with redirection back to edit-GET if there are errors.  Notice also, that the "cancel" check comes before the error check.

The delete-GET method follows a similar structure to the edit-POST, but a form is not used in the Thymeleaf page.  There are three actions/phases in this method: "cancel", "confirm", and "stage".  The "stage" phase displays the initial delete page, while the other two phases provide functionality after the page is displayed.


4. The updated messaged_en.properties file is next.  The HTML pages contain a number of static references, that point back to the messages_en.properties file.  This contains static text that was updated during this exercise.  The contents of this file is shown below.
admin.page.title=Admin Home Page

button.label.update=Update
button.label.add.strategy=Add Strategy
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.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=Actions
strategy.list.add.title=Add Strategy

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

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

5. The Thymeleaf HTML pages are updated next.  Additionally, the pages have been updated to use the Thymeleaf "data-th" tags rather than the "th:" tags.  The former being HTML5 compliant.  The first page to be updated is the list page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <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/css/dashboard.css" data-th-href="@{/resources/css/dashboard.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]-->
</head>
<body>
 <div class="container-fluid">
  <div class="row">
   <div class="main">
    <h3 data-th-text="#{strategy.list.table.title}">Configured Strategies</h3>
    <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>  </td>
        <td class="text-danger" data-th-if="${#fields.hasErrors('type')}" data-th-errors="*{type}">type error</td>
        <td class="text-danger" data-th-if="${#fields.hasErrors('name')}" data-th-errors="*{name}">name error</td>
        <td>  </td>
       </tr>
      </tbody>
     </table>
    </div>
    </form>
    
   </div>  <!-- END MAIN TAG -->
  </div>  <!-- END ROW TAG -->
 </div> <!-- END CONTAINER TAG -->

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" 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 type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" data-th-href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <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/css/dashboard.css" data-th-href="@{/resources/css/dashboard.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]-->
</head>
<body>
 <div class="container-fluid">
  <div class="row">
   <div class="main">
   
    <div class="col-sm-3"></div>
    <div class="col-sm-6">
     <h3 data-th-text="#{strategy.edit.head.title}">Edit Strategy</h3><br />
     <form class="form-horizontal" action="#" data-th-action="@{/strategy/edit}" data-th-object="${strategy}" method="post">
      <div class="form-group">
       <label class="col-sm-5 control-label" data-th-text="#{strategy.list.type.label}">Strategy Type</label>
       <div class="col-sm-7">
        <input type="text" hidden="hidden" data-th-value="*{id}" data-th-field="*{id}" ></input>
        <input type="text" class="form-control" data-th-value="*{type}" data-th-field="*{type}" ></input>
       </div>
      </div>
      <div class="form-group">
       <label class="col-sm-5 control-label" data-th-text="#{strategy.list.name.label}">Strategy Name</label>
       <div class="col-sm-7">
        <input type="text" class="form-control" data-th-value="*{name}" data-th-field="*{name}" ></input>
       </div>
      </div>
      <div class="form-group">
       <div class="col-sm-offset-5 col-sm-7" >
        <button type="submit" class="btn btn-primary"        name="action" data-th-value="#{button.action.save}"   data-th-text="#{button.label.save}"  >Save</button>
        <button type="submit" class="btn btn-default active" name="action" data-th-value="#{button.action.cancel}" data-th-text="#{button.label.cancel}">Cancel</button>
       </div>
      </div>
      <div class="form-group">
       <div class="col-sm-offset-5 col-sm-7" >
        <p class="text-danger" data-th-if="${#fields.hasErrors('type')}" data-th-errors="*{type}">type error</p> 
        <p class="text-danger" data-th-if="${#fields.hasErrors('name')}" data-th-errors="*{name}">name error</p> 
       </div>
      </div>
     </form>
    </div>
    <div class="col-sm-3"></div>
    
   </div>  <!-- END MAIN TAG -->
  </div>  <!-- END ROW TAG -->
 </div> <!-- END CONTAINER TAG -->

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" 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 type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" data-th-href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
The "fields.hasErrors" lines in both of the above HTML files are used to display error messages produced from the @Valid call in the controller.


6.  At this point the application is ready to test.  If you run the application and navigate to the list and edit pages you can verify the validation.  Screenshots are below.








In future posts we will look at modal dialogues, navigation/themes/Thymeleaf templates, 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-ex06-validation

Friday, February 21, 2014

Spring MVC 4 - Thymeleaf and Bootstrap

In this post, we build on the example that we finished in the last post, Spring MVC 4 - Thymeleaf CRUD - Part 4.  We will integrate Twitter Bootstrap into our Spring/Thymeleaf CRUD application.

1. The first step is to download both Twitter Bootstrap and jQuery.  I installed versions 3.1.1 and 1.11.0 respectively.  The download links are:

2. Extract the artifacts from the zip files and copy them to the "resources" directory under "webapp".  Also, delete the old css file, style.css.  Your directory structure should look like the image below, with the directories "css", "fonts", and "js" populated.

3. Now we will need to modify each of our view pages.  Besides adding the css and js file references to our Thymeleaf files, we ill need to rework the html to include the div tags commonly used by Bootstrap.


4. The reworked home.html page is show below.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" th:href="@{/resources/css/bootstrap-3.1.1.min.css}" />

 <title th:text="#{user.page.title}">Title</title>
 
 <!-- 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]-->
</head>

<body>
 <h1 data-th-text="#{home.page.title}">Title</h1>
 <p>
 <a href="strategy/list.html">Strategy list</a><br/>
 </p>

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" src="../../resources/js/jquery-1.11.0.min.js" th:href="@{/resources/js/jquery-1.11.0.min.js}"></script>
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" th:href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
In this page, we included the Bootstrap Template components, as we will on the other three html pages.  The head section of the page is entirely the Bootstrap Template, other than the title and Thymeleaf references.  Just before the body tag is closed, the two Bootstrap required js references are included, as suggested in teh Bootstrap Template.


5. The reworked strategy-list.html page is show below.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" th:href="@{/resources/css/bootstrap-3.1.1.min.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]-->
</head>
<body>
 <div class="container-fluid">
  <div class="row">
   <div class="main">
    <h3 data-th-text="#{strategy.list.table.title}">Configured Strategies</h3>
    <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>

    <form class="form" action="#" data-th-action="@{/strategy/add}" data-th-object="${strategyDTO}" 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="#{add.strategy.button.label}">Add Strategy</button>
        </td>
       </tr>
      </tbody>
     </table>
    </div>
    </form>
    
   </div>  <!-- END MAIN TAG -->
  </div>  <!-- END ROW TAG -->
 </div> <!-- END CONTAINER TAG -->

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" src="../../resources/js/jquery-1.11.0.min.js" th:href="@{/resources/js/jquery-1.11.0.min.js}"></script>
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" th:href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
A lot of changes were made to this page, with many of the changes a result of trial-and-error.  At the top, you will notice a custom css style.  This was added to modify the appearance of the table responsible for adding a strategy.

In the tables you will notice the addition of glyphicons, to make button functionality more clear.  You will also see the inclusion of div tags and liberal references to class attributes on many of the tags.  These two items are responsible for the sizing and appearance of the page.  In order to better understand how much space a given div occupied, I would add the following attribute inside the div tag in question: style="background:#888888;"


6. The reworked strategy-edit.html page is show below.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" th:href="@{/resources/css/bootstrap-3.1.1.min.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]-->
</head>
<body>
 <div class="container-fluid">
  <div class="row">
   <div class="main">
   
    <div class="col-sm-3"></div>
    <div class="col-sm-6">
     <h3 data-th-text="#{strategy.edit.head.title}">Edit Strategy</h3>
     <form class="form-horizontal" action="#" data-th-action="@{/strategy/edit}" data-th-object="${strategyDTO}" method="post">
      <div class="form-group">
       <label class="col-sm-4 control-label" data-th-text="#{strategy.list.type.label}">Strategy Type</label>
       <div class="col-sm-8">
        <input type="text" hidden="hidden" data-th-value="*{id}" data-th-field="*{id}" ></input>
        <input type="text" class="form-control" data-th-value="*{type}" data-th-field="*{type}" ></input>
       </div>
      </div>
      <div class="form-group">
       <label class="col-sm-4 control-label" data-th-text="#{strategy.list.name.label}">Strategy Name</label>
       <div class="col-sm-8">
        <input type="text" class="form-control" data-th-value="*{name}" data-th-field="*{name}" ></input>
       </div>
      </div>
      <div class="form-group">
       <div class="col-sm-offset-4 col-sm-8" >
        <button type="submit" class="btn btn-primary" name="action" value="save" data-th-text="#{update.button.label}">Save</button>
        <button type="submit" class="btn btn-default active" name="action" value="cancel" data-th-text="#{cancel.button.label}">Cancel</button>
       </div>
      </div>
     </form>
    </div>
    <div class="col-sm-3"></div>
    
   </div>  <!-- END MAIN TAG -->
  </div>  <!-- END ROW TAG -->
 </div> <!-- END CONTAINER TAG -->

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" src="../../resources/js/jquery-1.11.0.min.js" th:href="@{/resources/js/jquery-1.11.0.min.js}"></script>
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" th:href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
As with the list page, the edit page makes heavy use of div tags and the class attribute.  Many of the class attributes are used for sizing, with the sizing values looking like "col-sm-#".  With nearly all Bootstrap pages you will notice a common page structure inside the html body tag:

   div class = container
      div class = row
         div class = main
            ... content here
         div
      div
   div

The page above is no exception.  Also notice the button class attribute values and the class attribute value of "form-group" in the page.  The latter does exactly what it sounds like, grouping functionality within a form.


7. The reworked strategy-delete.html page is show below.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
   xmlns:th="http://www.thymeleaf.org">

<head data-th-fragment="header">
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title data-th-text="#{strategy.list.page.title}">Title</title>
 
 <!-- Bootstrap -->
 <link type="text/css" rel="stylesheet" href="../../resources/css/bootstrap-3.1.1.min.css" th:href="@{/resources/css/bootstrap-3.1.1.min.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]-->
</head>
<body>
 <div class="container-fluid">
  <div class="row">
   <div class="main">

    <div class="col-sm-3"></div>
    <div class="col-sm-6">
     <h3 data-th-text="#{strategy.delete.head.title}">Delete Strategy</h3>
     <form class="form-horizontal" action="#" method="get">
      <div class="form-group">
       <label class="col-sm-4 control-label" data-th-text="#{strategy.list.id.label}">Strategy Id</label>
       <div class="col-sm-8">
        <input type="text" class="form-control" data-th-field="${strategyDTO.id}" disabled="disabled"></input>
       </div>
      </div>
      <div class="form-group">
       <label class="col-sm-4 control-label" data-th-text="#{strategy.list.type.label}">Strategy Type</label>
       <div class="col-sm-8">
        <input type="text" class="form-control" data-th-field="${strategyDTO.type}" disabled="disabled"></input>
       </div>
      </div>
      <div class="form-group">
       <label class="col-sm-4 control-label" data-th-text="#{strategy.list.name.label}">Strategy Name</label>
       <div class="col-sm-8">
        <input type="text" class="form-control" data-th-field="${strategyDTO.name}" disabled="disabled"></input>
       </div>
      </div>
     </form>
     <div class="form-horizontal">
      <div class="form-group">
       <label class="col-sm-4 control-label"></label>
       <div class="col-sm-8" >
        <a href="#" data-th-href="@{/strategy/delete(id=${strategyDTO.id},phase=confirm)}">
                 <button type="button" class="btn btn-primary" data-th-text="#{delete.button.label}">Delete</button></a>
                 
        <a href="#" data-th-href="@{/strategy/delete(id=${strategyDTO.id},phase=cancel)}">
                 <button type="button" class="btn btn-default active" data-th-text="#{cancel.button.label}">Cancel</button></a>
                </div>
      </div>
     </div>
    </div>
    <div class="col-sm-3"></div>
   </div>  <!-- END MAIN TAG -->
  </div>  <!-- END ROW TAG -->
 </div> <!-- END CONTAINER TAG -->

 <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
 <script type="text/css" src="../../resources/js/jquery-1.11.0.min.js" th:href="@{/resources/js/jquery-1.11.0.min.js}"></script>
 <!-- Include all compiled plugins (below), or include individual files as needed -->
 <script type="text/css" src="../../resources/js/bootstrap-3.3.3.min.js" th:href="@{/resources/js/bootstrap-3.3.3.min.js}"></script>
</body>
</html>
There are two key items to note on this page.  We used an input field for a consistent look-and-feel even though we do not allow input on this page.  In order to make the fields read-only, we used the Bootstrap input tag attribute of disabled="disabled".

The other item to note is how a button was used with the "a href=..." tag/attribute.  In order to maintain a consistent look-and-feel, the following button class attributes were used:
  • class="btn btn-primary"
  • class="btn btn-default active"

8. Now, let's take a look at our new pages, starting with the new home.html page.

9. Here is our new strategy-list.html page.

10. And our strategy-edit.html page.

11. And finally our strategy-delete.html page.

In future posts we will look at input validation, modal dialogues, navigation/themes/Thymeleaf templates, integrating Spring Security, JPA/connection pooling, and unit testing.  I changed this list slightly after working on this exercise.  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-ex05