Unit Testing Using MySQL In-Memory Database On Spring

Well the title lied, there’s no such thing as in-memory MySQL database (or at least I won’t be using it for this article). Instead I will use H2 in-memory database setup to run in “MySQL mode”

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" >
  <property name="driverClassName" value="org.h2.Driver"/>
  <property name="url" value="jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" />
</bean>

(thanks to joensson for sharing this technique on SO)

If your app just uses plain jdbc then adding above datasource to your test context would be sufficient, but if you use JPA/Hibernate the cost of table setup, scanning etc could be quite significant.

To overcome this you can split the test context using @ContextHierarchy annotation.

In the example below I have unit tests for two DAOs: AccountDAOTest and CustomerDAOTest:

@ContextHierarchy({
  @ContextConfiguration("/test-root-context.xml"),
  @ContextConfiguration("AccountDAOTest-context.xml")
})
@RunWith(SpringJUnit4ClassRunner.class)
public class AccountDAOTest {
}
@ContextHierarchy({
  @ContextConfiguration("/test-root-context.xml"),
  @ContextConfiguration("CustomerDAOTest-context.xml")
})
@RunWith(SpringJUnit4ClassRunner.class)
public class CustomerDAOTest {
}

By doing this Spring test-root-context.xml will be setup once and reused accross all unit tests. Only put components common to all tests in test-root-context.xml. In my case I put the following:

  • DataSource
  • EntityManagerFactory
  • TransactionManager
<!-- JDBC Data Source -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="org.h2.Driver"/>
  <property name="url" value="jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" />
</bean>


<!-- EntityManagerFactory -->
<bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
  <property name="persistenceUnitName" value="persistenceUnit" />
  <property name="dataSource" ref="dataSource" />
</bean>

<!-- Transaction Manager -->
<bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
  <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

All test specific components go into their respective context.

Don’t forget to add <tx:annotation-driven/> if your DAO uses it. This can’t be placed on test-root-context.xml because I don’t scan all my DAOs there.

And lastly — ofcourse — you need to make sure your pom.xml has dependency to spring-test, junit and h2


<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.3.176</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>${org.springframework-version}</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.7</version>
  <scope>test</scope>
</dependency>

Using Custom Role Populator On Spring Security LDAP

Here’s how you can use Spring Security LDAP just for password authentication, but use your own database for assigning role / authorities.

First on your context xml define an LDAP server context bean as per normal:

<ldap-server id="ldapContext" url="ldap://myldapserver:389" 
    manager-dn="CN=Joe Admin,OU=SBSUsers,OU=Users,OU=MyBusiness,DC=hello,DC=com" 
    manager-password="joe1234"
    root="dc=hello,dc=com"
    />

Then define LDAPAuthenticationProvider bean:

<beans:bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
  <beans:constructor-arg name="authenticator">
    <beans:bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
      <beans:constructor-arg ref="ldapContext" />
      <beans:property name="userSearch">
        <beans:bean class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
          <beans:constructor-arg name="searchBase" value="ou=Users,ou=MyBusiness,dc=hello,dc=com"/>
          <beans:constructor-arg name="searchFilter" value="(sAMAccountName={0})"/>
          <beans:constructor-arg name="contextSource" ref="ldapContext"/>
        </beans:bean>
      </beans:property>
    </beans:bean>
  </beans:constructor-arg>
  <beans:constructor-arg name="authoritiesPopulator" ref="myLDAPAuthPopulator"/>
</beans:bean>

If you noticed at the bottom we set authoritiesPopulator into myLDAPAuthPopulator bean which we’ll define next. This is where you can lookup your database using jdbc or other method to populate the roles given an authenticated username:

@Component("myLDAPAuthPopulator")
public class MyLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {

  @Override
  public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    User user = userDAO.findByUsername(username);
    for(String role : user.getRoles()) {
      authorities.add(new SimpleGrantedAuthority(role));
    }
    return authorities;
  }

}

And finally register this authentication provider in the authentication manager:

<authentication-manager>
  <authentication-provider ref="ldapAuthProvider"/>
</authentication-manager>

Showing Geographical Location On Google Map (Geocoding)

Translating a geographical location such as “London, UK” into a lat/long coordinate (lat: 51.5085150, long: -0.1254870) is called geocoding. This coordinate can then be used to display the location on google map.

Try putting a location in the box below and hit go:

Register for Google Map API Key

In order to implement the above widget first obtain an API key from Google:

  1. Go to https://code.google.com/apis/console
  2. Select Services on the left menu, ensure Geocoding API, Google Maps Embed API and Google Maps JavaScript API v3 are turned on

    geocoding1

    geocoding2
  3. Go to API Access, select Create new Browser key

    geocoding3
  4. The list of accepted HTTP referers will be asked, if you’re testing on local machine this would probably be localhost/*, otherwise you need to put your website domain name here. Hit Create once you’re done

    geocoding4
  5. Take a note of your API key, this will be used in the later part of this tutorial

    geocoding5

The HTML Code

This widget only contains 3 elements:

  • A text input box to provide the geographical location
  • A button to submit the address and show it on the map
  • A div container for the map
Enter location (eg: London, UK): <input type="text" id="loc"/>
<button id="go">Go</button>

<div id="map" style="height:300px; width: 300px;"></div>

Javascript

2 javascript libraries are used: jQuery and Google Map JS. The key parameter on the Google Map JS URL should be the key created above.

<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript"
  src="https://maps.googleapis.com/maps/api/js?key=AIzaSyA17RMo75QCidF3_FxbwhLCHqhnylQEdws&
sensor=false">

Firstly let’s initialize the div container so it doesn’t come up with blank map, here I set it into coordinate of San Francisco, CA:

$(document).ready(function() {
	var map = new google.maps.Map($('#map')[0], {
		center: new google.maps.LatLng(37.7749295, -122.4194155),
		zoom: 8
	});
	...
}

And create a click handler for the ‘Go’ button which sends ajax post into Google Map JS API. The API will return a JSON response with the location if successful.

$(document).ready(function() {
	...
	$('#go').click(function(e) {
		e.preventDefault();
		var loc = $('#loc').val();
		$.ajax({
			url: '//maps.googleapis.com/maps/api/geocode/json?sensor=false&address='+loc
		}).done(function(data) {
			if(data.status != "OK") {
				alert("Unable to find location");
				return;
			}
			var loc_result = data.results[0].geometry.location;
			map.setCenter(new google.maps.LatLng(loc_result.lat, loc_result.lng));
		});
	});
});

See the widget in action in full screen.

Showing Spring Security Principal and Conditional Logic on JSP

If your app is setup with Spring Security, you can show currently logged in user in JSP by using:

${pageContext.request.userPrincipal.name}

You can also check if the user has enough right to access a resource. This example ensures user has enough rights to access /admin or else the link won’t be rendered:

<sec:authorize url="/admin">
  <li><a href="/admin">Admin Home</a></li>
</sec:authorize>

Make sure you have spring-security-taglibs on your classpath:

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-taglibs</artifactId>
  <version>${org.springframework.security-version}</version>
</dependency>

And the tag prefix declared on top of the JSP page:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

AWS EC2 Nginx Reverse Proxy And localhost Slowness

This is something really odd I yet to fully understand, but for time being I’ve decided using localhost on AWS ec2 is bad. (at least on Windows Server 2008 R2)

I think it might have something to do with internal DNS or address routing, but my nginx-reverse-proxied tomcat is 4-5x slower when I bind it into localhost as opposed of the local IP. We’re talking 1 minute to load a 300kb css file!

Open a command prompt and run ipconfig /all and your local ip should be under IPv4 Address under Ethernet adapter Local Area Connection 2:

ec2-local-ip

On tomcat, edit your server.xml, find your <Connector> element and add address attribute:

<Connector port="8080" protocol="HTTP/1.1"
    connectionTimeout="20000"
    redirectPort="8443" address="10.172.1.1"/>

And finally update nginx.conf to point the reverse proxy backend to above IP.

After doing this now my reverse proxy is much faster, only few seconds to load 300kb css file.

How To Upload Your Project to Github

Warning
Using free Github account will cause your project to be visible publicly

Often you’re solving a very tricky programming problem and it’s hard to describe it on forum. One very useful thing to do is to isolate the problem by creating a small project and share it. This can be done easily using git and github.

  1. Install git if your PC/Mac doesn’t come with one. Use git --version on a terminal shell / command prompt to check.
  2. Sign up for a github account if you haven’t got one
  3. To avoid having to type in password each time you upload / sync code to github, let’s setup public/private key authentication. This only need to be setup once per PC. First check if you already have a ssh keypair. On windows: open explorer > right click > git bash (or just open a terminal shell on OSX / Linux) > check if you have the file ~/.ssh/id_rsa and ~/.ssh/id_rsa.pub. If you don’t, generate a new one using ssh-keygen (accept all the default setting)
  4. On github go to account settings > SSH Keys > Add SSH Keys. Paste the content of ~/.ssh/id_rsa.pub.
  5. Create new github repository. Give it your project name (eg: foobar).
    github
  6. Back in your computer, point your git shell to the root of your project and run these commands:
    $ git --init
    # You may want to create a .gitignore file before proceeding to
    # next command, see below
    $ git add -A
    $ git commit -m 'initial commit'
    # Change mygitusername and foobar
    $ git remote add origin git@github.com:mygitusername/foobar.git
    $ git push -u origin master
    

    It’s a good practice to add a .gitignore to avoid tracking binaries and project metadata. Do this before git add -A. This is my typical .gitignore file for a maven java project:

    # All files paths matching these prefix will not be tracked by git
    target
    .settings
    .project
    .classpath
    .springBeans
    
  7. Congratz, you project is now up in github. It’s also a good practice to add a README.md file to include instruction on how to run your project. Run following commands for subsequent changes you want to push to github:
    $ git add -A
    $ git commit -m 'fixed bugs abcd'
    $ git push
    

Spring Security Auto Login After Successful Registration

Asking your user to login immediately after he/she registers might be tedious. Here’s how you can log them in immediately using Spring Security (thanks to this SO thread).

Typically you will have some sort of registration form with a backing controller like this:

@Controller
@RequestMapping("/register")
public class RegisterController {
  ...
  @RequestMapping(method = POST)
  public String register(@ModelAttribute User user) {
    // perform registration logic..
    // redirect back to login page
    return "redirect:/login";
  }
  ...
}

But a server-side login can be done by autowiring UserDetailService and AuthenticationManager:

@Controller
@RequestMapping("/register")
public class RegisterController {
  ...
  @Autowired @Qualifier("authMgr") private AuthenticationManager authMgr;
  @Autowired private UserDetailsService userDetailsSvc;

  @RequestMapping(method = POST)
  public String register(@ModelAttribute User user) {
    // perform registration logic..

    // perform login authentication
    try {
      UserDetails userDetails = userDetailsSvc.loadUserByUsername(username);
      UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
      authMgr.authenticate(auth);

      // redirect into secured main page if authentication successful
      if(auth.isAuthenticated()) {
        SecurityContextHolder.getContext().setAuthentication(auth);
        return "redirect:/";
      }
    } catch (Exception e) {
      logger.debug("Problem authenticating user" + username, e);
    }

    return "redirect:/error";
  }
  ...
}

Note that in above code the AuthenticationManager injection is qualified by @Qualifier("authMgr"). This is to avoid multiple beans ambiguity. In effect in the xml context configuration (if you use one) an id attribute has to be set:

  ...
  <authentication-manager id="authMgr">
    ...
  </authentication-manager>

  <http authentication-manager-ref="authMgr">
    ...
  </http authentication-manager-ref>
  ...

Also in order for this setup to work, the registration page has to be filtered by spring security

  ...
  <!-- This is wrong, security context will not be available if this path is set to "none" -->
  <!-- <http pattern="/register/**" security="none"/> -->

  <http authentication-manager-ref="authMgr">
    <intercept-url pattern="/register/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
  </http authentication-manager-ref>
  ...

See Also

Installing Spring Security On Spring MVC Project

IP Whitelisting wp-admin In Nginx

Similar like Apache, Nginx also has allow & deny directives allowing you to block certain ip. Here’s a config I use to whitelist /wp-admin to certain IP only.

location / {
  try_files $uri $uri/ /index.php?$args;
}

location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
  access_log off; log_not_found off; expires max;
}

location ~ \.php$ {
  try_files                $uri =404;
  fastcgi_pass             localhost:9000;
  fastcgi_index            index.php;
  fastcgi_param            SCRIPT_FILENAME  $document_root$fastcgi_script_name;
  fastcgi_intercept_errors on;
  include                  fastcgi_params;
}

location ~ ^/wp-(admin|login) {
  allow 110.78.34.102/32;
  deny  all;
  location ~ \.php$ {
    try_files                $uri =404;
    fastcgi_pass             localhost:9000;
    fastcgi_index            index.php;
    fastcgi_param            SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    fastcgi_intercept_errors on;
    include                  fastcgi_params;
  }
}

Apart from any path starting with /wp-admin, this will also restrict /wp-login.php to specified IP only.

Hopefully this come handy when you’re configuring wordpress using nginx php-fpm.

Using HandlerInterceptor or ControllerAdvice to Make Spring MVC Model Attribute Available Everywhere

Often you’ll want a particular model attribute to be available everywhere (eg: application version, name, etc). Doing model.addAttribute() everytime is a really bad idea, if you have to refactor it down the track you could end up with hundreds of line modification scattered everywhere.

Using ControllerAdvice

One simple way to achieve this as outlined by Spring documentation is by using Spring 3.1′s @ControllerAdvice annotation.

Create a new class like this:

@ControllerAdvice
public class PopulateGlobalAttribute {
  @ModelAttribute("appversion")
  public String getAppVersion() {
    return "1.0";
  }
}

And the getAppVersion() will be used to help each handler method in all controllers to add additional stuff to Model object.

However this method poses one problem. If the handler method is returning a redirect view, the model attributes will be exposed as a query string on the browser URL.

Using HandlerInterceptor

Fortunately Josh Kalderimis proposes a nice solution on his blog post using HandlerInterceptor.

This is my own version of HandlerInterceptor which is based from Josh’s:

public class PopulateGlobalAttrInterceptor implements HandlerInterceptor {
  private Map<String, String> properties = new HashMap<String, String>();

  /**
   * This method ensures the global attributes are added only for non-redirection view / view name
   */
  @Override
  public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler,
      ModelAndView mav) throws Exception {
    if(mav == null) return;

    boolean isRedirectView = mav.getView() instanceof RedirectView;
    boolean isViewObject = mav.getView() != null;
    boolean viewNameStartsWithRedirect = (mav.getViewName() == null ? true : 
      mav.getViewName().startsWith(UrlBasedViewResolver.REDIRECT_URL_PREFIX));
    
    if(mav.hasView() && (
      ( isViewObject && !isRedirectView) ||
      (!isViewObject && !viewNameStartsWithRedirect))){
        addCommonModelData(mav);
    }
  }

  private void addCommonModelData(ModelAndView mav) {
    mav.getModel().putAll(properties);
  }

  @Override
  public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler,
      Exception ex) throws Exception {
  }

  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler)
      throws Exception {
    return true;
  }

  /* getters & setters */
}

In SpringMVC HandlerInterceptors are similar like servlet filters, it will be used to filter (intercept) through every requests. Similar like ControllerAdvice, the postHandle method will be invoked by all handlers to help populate the model.

Once you have this you need to register this interceptors. If you use xml based configuration you can do something like this:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
  ...

  <mvc:annotation-driven/>

  <bean id="populateGlobalAttrInterceptor" class="myapp.PopulateGlobalAttrInterceptor">
    <property name="properties">
      <map>
        <entry key="appversion" value="1.0"/>
      </map>
    </property>
  </bean>

  <mvc:interceptors>
    <ref bean="populateGlobalAttrInterceptor"/>
  </mvc:interceptors>
  ...
</beans>

Now without any additional code, you’re guaranteed ${appversion} is available everywhere on your jsp view.

Stock Ticker Demo Webapp Using Spring 4 Websocket

In case you haven’t heard what Websocket is, long story short it’s a brand new cool technique of asynchronous client-server communication for web application. Instead of periodic / long ajax polling, newer browsers allow you to have a persistent socket (almost like TCP) where both client and server can send messages anytime.

Yes the all-new Spring 4 came with shiny Websocket support! Here’s a stock ticker app to get you started (thanks to raymondhlee’s article for the inspiration). This app will let you add/remove a stock code and update its price every second (by randomly adding / subtracting some percentage)

stockticker

Environment / Tools

  • Java 7
  • Tomcat 7.0.47
  • Servlet API 3
  • Spring Framework 4.0.2.RELEASE
  • SockJS 0.3
  • StompJS

Maven Dependencies

Most dependencies are similar to Spring MVC but there’s few addition required to support websocket. You also need to use Servlet 3.

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>${org.springframework-version}</version>
</dependency>

<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.1.0</version>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.3.2</version>
</dependency>

web.xml

You need to use version 3 of the web.xml schema, and on DispatcherServlet config, set <async-supported>true</async-supported>. Here’s src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <!-- Processes application requests -->
  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>

  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

Setup Websocket Message Broker On Servlet Context XML

Apart from the standard Spring MVC config, one new stuff we’re introducing is the Websocket Message Broker. The message broker will help us listening, mapping and sending messages. Note that as suggested by Spring docs we’re using STOMP message protocol and SockJS to support non-websocket browser.

Also note following configs:

  • Stomp endpoint: /ws
  • Message broker app prefix: /app
  • Message queue prefix: /topic

Here’s src/main/webapp/WEB-INF/servlet-context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:websocket="http://www.springframework.org/schema/websocket"
  xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  <mvc:annotation-driven />

  <mvc:resources mapping="/resources/**" location="/resources/" />

  <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
  </bean>

  <context:component-scan base-package="com.gerrydevstory.stockticker" />

  <websocket:message-broker application-destination-prefix="/app">
    <websocket:stomp-endpoint path="/ws">
      <websocket:sockjs/>
    </websocket:stomp-endpoint>
    <websocket:simple-broker prefix="/topic"/>
  </websocket:message-broker>

</beans>

The Domain Object

Stock class is a simple POJO with code, price and time fields. I’ve also added getTimeStr() to format the time as string and additional constructor.

package com.gerrydevstory.stockticker;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Stock implements Serializable {

  private static final long serialVersionUID = 1L;
  private String code = "";
  private double price = 0.0;
  private Date time = new Date();
  
  public Stock() {
    
  }
  
  public Stock(String code, double price) {
    this.code = code;
    this.price = price;
  }
  
  private DateFormat df = new SimpleDateFormat("dd MMM yyyy, HH:mm:ss");
  
  public String getTimeStr() {
    return df.format(time);
  }
  
  /* standard getters & setters */
  
}

Broadcast Prices And Add / Remove Stock

At the core of this app is the HomeController class. There’s a updatePriceAndBroadcast() method which is scheduler to run every 1 second using TaskScheduler. This controller also has websocket handler method to add new stock and remove all. Note the usage of @MessageMapping annotation, it will make more sense once we go through the javascript part below.

package com.gerrydevstory.stockticker;

@Controller
public class HomeController {

  @Autowired private SimpMessagingTemplate template;  
  private TaskScheduler scheduler = new ConcurrentTaskScheduler();
  private List<Stock> stockPrices = new ArrayList<Stock>();
  private Random rand = new Random(System.currentTimeMillis());
  
  /**
   * Iterates stock list, update the price by randomly choosing a positive
   * or negative percentage, then broadcast it to all subscribing clients
   */
  private void updatePriceAndBroadcast() {
    for(Stock stock : stockPrices) {
      double chgPct = rand.nextDouble() * 5.0;
      if(rand.nextInt(2) == 1) chgPct = -chgPct;
      stock.setPrice(stock.getPrice() + (chgPct / 100.0 * stock.getPrice()));
      stock.setTime(new Date());
    }
    template.convertAndSend("/topic/price", stockPrices);
  }
  
  /**
   * Invoked after bean creation is complete, this method will schedule 
   * updatePriceAndBroacast every 1 second
   */
  @PostConstruct
  private void broadcastTimePeriodically() {
    scheduler.scheduleAtFixedRate(new Runnable() {
      @Override public void run() {
        updatePriceAndBroadcast();
      }
    }, 1000);
  }
  
  /**
   * Handler to add one stock
   */
  @MessageMapping("/addStock")
  public void addStock(Stock stock) throws Exception {
    stockPrices.add(stock);
    updatePriceAndBroadcast();
  }
  
  /**
   * Handler to remove all stocks
   */
  @MessageMapping("/removeAllStocks")
  public void removeAllStocks() {
    stockPrices.clear();
    updatePriceAndBroadcast();
  }
  
  /**
   * Serve the main page, view will resolve to /WEB-INF/home.jsp
   */
  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String home() {
    return "home";
  }

}

Client Side Stuff

To render the stock prices, I created an empty HTML table. The idea is we will empty the table and fill it in with new prices per update

<table>
  <thead><tr><th>Code</th><th>Price</th><th>Time</th></tr></thead>
  <tbody id="price"></tbody>
</table>

Underneath that, I’ll also add few form input so you can add a new stock and remove everything

<p class="new">
  Code: <input type="text" class="code"/>
  Price: <input type="text" class="price"/>
  <button class="add">Add</button>
  <button class="remove-all">Remove All</button>
</p>

The javascript stuff is a bit complicated. Apart from JQuery, there are 2 libraries used here: StompJS and SockJS. As opposed of using direct API, SockJS provides fallback for older browser not supporting websocket. StompJS provides higher level abstraction of sending and receiving messages in STOMP protocol.

StompJS did not come with CDN, so I had to manually download it and place it on src/main/webapp/resources/stomp.js

<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script src="/stockticker/resources/stomp.js"></script>
<script src="https://code.jquery.com/jquery-1.11.0.min.js"></script>

Next is the inline script block. Here I used SockJS to connect to the Spring websocket STOMP endpoint /ws (recall servlet-context.xml above). My webapp context path is /stockticker.

//Create stomp client over sockJS protocol
var socket = new SockJS("/stockticker/ws");
var stompClient = Stomp.over(socket);

// Callback function to be called when stomp client is connected to server
var connectCallback = function() {
  stompClient.subscribe('/topic/price', renderPrice);
}; 

// Callback function to be called when stomp client could not connect to server
var errorCallback = function(error) {
  alert(error.headers.message);
};

// Connect to server via websocket
stompClient.connect("guest", "guest", connectCallback, errorCallback);

The connectCallback function above registers renderPrice callback when a message is sent to /topic/price. This function empties the result HTML table and re-add the cells with new stock price

// Render price data from server into HTML, registered as callback
// when subscribing to price topic
function renderPrice(frame) {
  var prices = JSON.parse(frame.body);
  $('#price').empty();
  for(var i in prices) {
    var price = prices[i];
    $('#price').append(
      $('<tr>').append(
        $('<td>').html(price.code),
        $('<td>').html(price.price.toFixed(2)),
        $('<td>').html(price.timeStr)
      )
    );
  }
}

And lastly, utilising JQuery let’s create handlers for adding and removig stocks

// Register handler for add button
$(document).ready(function() {
  $('.add').click(function(e){
    e.preventDefault();
    var code = $('.new .code').val();
    var price = Number($('.new .price').val());
    var jsonstr = JSON.stringify({ 'code': code, 'price': price });
    stompClient.send("/app/addStock", {}, jsonstr);
    return false;
  });
});

// Register handler for remove all button
$(document).ready(function() {
  $('.remove-all').click(function(e) {
    e.preventDefault();
    stompClient.send("/app/removeAllStocks");
    return false;
  });
});

Download And Try The Source Code

The source code of this demo app is available on github. Clone it using git:

git clone https://github.com/gerrytan/stockticker.git

Import it as Existing Maven Project in STS (File > Import > Existing Maven Project) and run on in-memory Tomcat 7.0.47 using following Run Configuration (Run > Run Configurations…):

tomcat-22-maven-sts

And this Tomcat container has to run on Java 7 to enable Websocket support.

tomcat-22-maven-sts-jdk

Gerry's software development journey of trial, errors and re-trials