How to Build Real Time Location Aware Java Applications using HTML5 GeoLocation API, WebSockets, JAX-RS, and MongoDB

One of the advantages of OpenShift or any other Platform as a Service is that it gives developers the power to turn their ideas into applications. As a developer, you are only concerned about writing code and the platform manages and scales the underlying infrastructure for you. I am also a developer and I love to write code.

A few days ago, I came up with a very simple idea to show messages in real-time on a map. A user posts a message via the application user interface, the application captures the user's current location using an HTML5 Geo-location API, and then displays the message on a map. If another user posts a message from some other part of world, the first user will see that same message in real-time. As users start posting messages, they will see all of the messages appearing on the map.

I had already built location-aware apps so I was quite comfortable in writing that part of the application but adding a real-time component was a new challenge for me. One way I could have added real-time capabilities to my application is by using OpenShift WebSockets support, but I thought it would be better if an API could provide me this capability. This is where OpenShift partners play a role. These partners augment OpenShift by providing useful services which OpenShift currently does not provide. You can view the full list of OpenShift partners. One of the OpenShift partners, Pusher, provides a cloud service which makes it easy to add real-time capabilities to any web or mobile app.

The application is already deployed and running on OpenShift so you can give it a try. Just open your web browser and go to http://messageonmap-cix.rhcloud.com/.

MessageOnMap application

Application Source code

The source code for this application is on github at https://github.com/shekhargulati/messagesonmap.

Prerequisite

Before we can start building the application, we'll have to do few setup tasks :

  1. Sign up for an OpenShift Account. It is completely free and Red Hat gives every user three free Gears on which to run your applications. At the time of this writing, the combined resources allocated for each user is 1.5 GB of memory and 3 GB of disk space.

  2. Install the rhc client tool on your machine. The rhc is a ruby gem so you need to have ruby 1.8.7 or above on your machine. To install rhc, just type

sudo gem install rhc

If you already have one, make sure it is the latest one. To update your rhc, execute the command shown below.

sudo gem update rhc

For additional assistance setting up the rhc command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install

  1. Setup your OpenShift account using rhc setup command. This command will help you create a namespace and upload your ssh keys to OpenShift server.

Create a scalable application

Lets start with creating a scalable JBossAS7 application to house our messageonmap application. To create a scalable application, all you need to do is to run the command shown below:

rhc app create messageonmap jbossas-7 mongodb-2.2 -s

The -s option informs OpenShift that you want to create a scalable application. Essentially this means that your application gear will be fronted with a HAProxy load balancer and MongoDB database will be hosted on a separate gear from your first application gear. If you want more detailed information on how scaling works, check out this excellent post.

Delete unwanted files

When you create any OpenShift application, the PaaS creates a templated application for you. We will delete the unwanted files because we don't need them.

$ git rm -rf src/main/webapp/snoop.jsp 
rm 'src/main/webapp/snoop.jsp'
 
$ git commit -am "deleted snoop.jsp"
[master 29879d8] deleted snoop.jsp
 1 file changed, 283 deletions(-)
 delete mode 100644 src/main/webapp/snoop.jsp

Adding required Maven dependencies

The Java application that OpenShift creates is a Maven based project. This application will be storing data in MongoDB so we need to add MongoDB Java driver dependency in our pom.xml. The Jackson dependency is required to convert Java object to JSON representation and the Apache HttpClient dependency is required to interact with the Pusher REST API. I was not able to find Pusher Java client in Maven central.

<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongo-java-driver</artifactId>
    <version>2.10.1</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.2.5</version>
</dependency>

Persisting messages in MongoDB

Our domain model is very simple and it just consists of one domain object -- Message. Message has two fields -- message to capture text and location double array to capture user location.

Message

public class Message {
 
    private double[] location;
    private String message;
 
    public Message() {
        // TODO Auto-generated constructor stub
    }
 
    public Message(double[] location, String message) {
        this.location = location;
        this.message = message;
    }
    ..

Next we will create an application scoped bean to manage MongoDB database connection. The connection class works on both the local system as well as on OpenShift.

DBConnection

@Named
@ApplicationScoped
public class DBConnection {
 
    private DB mongoDB;
 
    @PostConstruct
    public void afterCreate() {
        System.out.println("just see if we can say anything");
 
        String host = System.getenv("OPENSHIFT_MONGODB_DB_HOST");
 
        if (host == null || "".equals(host)) {
            // local MongoDB connection
 
        } else {
 
            // on openshift
            String mongoport = System.getenv("OPENSHIFT_MONGODB_DB_PORT");
            String user = System.getenv("OPENSHIFT_MONGODB_DB_USERNAME");
            String password = System.getenv("OPENSHIFT_MONGODB_DB_PASSWORD");
            String db = System.getenv("OPENSHIFT_APP_NAME");
            int port = Integer.decode(mongoport);
 
            Mongo mongo = null;
            try {
                mongo = new Mongo(host, port);
                mongo.setWriteConcern(WriteConcern.SAFE);
            } catch (UnknownHostException e) {
                System.out.println("Couldn't connect to Mongo: "
                        + e.getMessage() + " :: " + e.getClass());
            }
 
            mongoDB = mongo.getDB(db);
 
            if (mongoDB.authenticate(user, password.toCharArray()) == false) {
                System.out.println("Failed to authenticate DB ");
            }
        }
 
    }
 
    @Produces
    public DB getDB() {
        return mongoDB;
    }
 
 
}

An @ApplicationScoped bean will live for as long as the application is running and is destroyed when the application is shut down. This is exactly the behavior we want for the object holding on to the pooled connections from the MongoDB driver.

Enable CDI

CDI stands for Contexts and Dependency Injection. We are using CDI in our application so that we can use dependency injection instead of manually creating the objects ourselves. The CDI container will manage the bean lifecycle and the developer just has to write the business logic. To let the JBossAS7 application server know that we are using CDI, we need to create a file - beans.xml - in your WEB-INF directory. The file can be completely blank, but its presence tells the container that the CDI framework needs to be loaded. The beans.xml file is shown below:

<?xml version="1.0"?>
<beans 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://jboss.org/schema/cdi/beans_1_0.xsd"/>

RESTful backend for persisting messages

Now we will write a RESTful backend for our application using JAX-RS. We will activate JAX-RS by creating a class which extends javax.ws.rs.core.Application. The ApplicationPath annotation is used to specify the base URL under which your web service will be available. In the code shown below, I have used "/api" as base url.

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
 
@ApplicationPath("/api")
public class JaxRsActivator extends Application {
   /* class body intentionally left blank */
}

Once we have activated JAX-RS, we can write our REST service. The service shown below has just one POST REST Endpoint available at http://messageonmap-domain-name.rhcloud.com/messages. This endpoint is used to create messages in the application. It just gets the required values from the Message object and persists them to MongoDB.

@Path("/messages")
public class MessageRestService {
 
    @Inject
    private DB db;
 
    @Context
    private HttpServletRequest servletRequest;
 
    @POST
    @Consumes(value = MediaType.APPLICATION_JSON)
    public void submit(Message message) throws Exception {
        DBCollection messages = db.getCollection("messages");
        BasicDBObject object = new BasicDBObject();
        object.put("message", message.getMessage());
        object.put("location", message.getLocation());
        messages.insert(object);
    }
 
}

Client code to persist message

First we will create a file called Index.html which will house all the HTML required for creating this application. The page is HTML5 which you can verify by looking at its Doctype. It uses Twitter Bootstrap to create a sleek and responsive UI.

<!DOCTYPE html>
<html>
<head>
<title>MessageOnMap : Realtime Messages on the Map</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<style>
body {
    padding-top: 60px;
    padding-bottom: 100px;
}
#map-canvas {
    height: 500px;
}
</style>
<link href="css/bootstrap.css" rel="stylesheet">
<link href="css/bootstrap-responsive.css" rel="stylesheet">
<link href="css/jquery.loadmask.css" rel="stylesheet" type="text/css" />
 
</head>
<body>
 
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <button type="button" class="btn btn-navbar" data-toggle="collapse"
                    data-target=".nav-collapse">
                    <span class="icon-bar"></span> <span class="icon-bar"></span> <span
                        class="icon-bar"></span>
                </button>
                <a class="brand" href="#">MessageOnMap</a>
                <div class="nav-collapse collapse">
                    <ul class="nav">
                        <li class="active"><a href="#">Home</a></li>
                    </ul>
                </div>
 
                <!--/.nav-collapse -->
            </div>
        </div>
    </div>
 
    <div id="main" class="container">
            <div class="row">
            <div class="span12">
                    <div class="hero-unit">
                        <h2>MessageOnMap</h2>
                        <small>Realtime messaging on the map</small>
                        <p>The App allows you to post messages in realtime messaging
                            on the map. It is an awesome way to discover people near you.</p>
                    </div>
                </div>
            </div>
 
            <div class="row">
                <div class="span4">
                    <form id="messageForm" class="form" method="POST">
                        <div class="control-group">
                            <div class="controls">
                                <textarea id="message" name="message" class="input-xlarge"
                                    placeholder="Say Something !!" required rows="5"></textarea>
                            </div>
                        </div>
 
                        <div class="control-group">
                            <div class="controls">
                                <button type="submit" class="btn btn-success">Render Me on Map</button>
                            </div>
                        </div>
 
                    </form>
                </div>
 
                <div id="map-canvas" class="span8"></div>
            </div>
 
        <footer>
            <p>&copy; Shekhar Gulati 2013</p>
        </footer>
 
 
    </div>
 
    <script src="js/jquery.js"></script>
    <script type="text/javascript" src="js/jquery.loadmask.min.js"></script>
    <script src="https://maps.googleapis.com/maps/api/js?sensor=true"></script>
 
    <script type="text/javascript">
        if (!navigator.geolocation) {
            alert('Your browser does not support geolocation. Please download latest browser version.');
            $("button").attr("disabled", "disabled");
        }
    </script>
 
 
    <script src="js/app.js"></script>
</body>
</html>

All the application specific JavaScript code is in the app.js file. It first creates a map object when the page loads and binds to the form submit event. When the form is submitted, it first gets the users current location using the HTML5 GeoLocation API and then makes a POST call to api/messages to persist the message.

var mapOptions = {
            zoom : 2,
            center : new google.maps.LatLng(40.46366700000001,
                    -3.7492200000000366),
            mapTypeControlOptions : {
                style : google.maps.MapTypeControlStyle.DROPDOWN_MENU
            },
            mapTypeId : google.maps.MapTypeId.ROADMAP,
            zoomControlOptions : {
                style : google.maps.ZoomControlStyle.SMALL
            }
};
 
var map = new google.maps.Map(document.getElementById('map-canvas'),mapOptions);
 
$("#messageForm").submit(function(event){
    event.preventDefault();
    $("#messageForm").mask("Sending Message ...");
    findUserCurrentLocation(saveMessage);
});
 
function findUserCurrentLocation(callback){
 
    navigator.geolocation.getCurrentPosition(function(position){
                    var longitude = position.coords.longitude;
                    var latitude = position.coords.latitude;
                    console.log('longitude .. '+longitude);
                    console.log('latitude .. '+latitude);
 
                    var location = new Array(longitude , latitude);
                    callback(location);
                }, function(e){
                    $("#messageForm").unmask();
                    switch (e.code) {
                        case e.PERMISSION_DENIED:
                            alert('You have denied access to your position. You will ' +
                                    'not get the most out of the application now.'); 
                            break;
                        case e.POSITION_UNAVAILABLE:
                            alert('There was a problem getting your position.'); 
                            break;
                        case e.TIMEOUT:
                                    alert('The application has timed out attempting to get ' +
                                            'your location.'); 
                            break;
                        default:
                            alert('There was a horrible Geolocation error that has ' +
                                    'not been defined.');
                    }
                },
                { timeout: 45000 }
 
                );
 
}
 
function saveMessage(location){
 
    var message = this.$('textarea#message').val();
 
    var url = "api/messages";
    var data = {message : message , location : location};
    $.ajax(url , {
        data : JSON.stringify(data),
        contentType : 'application/json',
        type : 'POST',
        success :function(){
            alert("Message Sent Successfully");
            $('#messageForm')[0].reset();
        },
        error :function(){
            alert("Message Delivery Failed");
        }
    });
 
    $("#messageForm").unmask();
 
}

Render persisted message on map

After the message is successfully saved, we render the message on the map.

function saveMessage(location){
 
 
    $.ajax(url , {
        . .. 
        success :function(){
            alert("Message Sent Successfully");
            renderMessageOnMap(data);
            $('#messageForm')[0].reset();
        },
        error :function(){
            alert("Message Delivery Failed");
        }
    });
 
    $("#messageForm").unmask();
 
}
 
function renderMessageOnMap(data){
    var latLng = new google.maps.LatLng(data.location[1], data.location[0]);
    map.setCenter(latLng);
    var marker = new google.maps.Marker({
        position : latLng,
        animation : google.maps.Animation.DROP,
        html : "<p>" + data.message + "</p>"
    });
 
    var infoWindow = new google.maps.InfoWindow();
    google.maps.event.addListener(marker, 'click', function() {
        map.setCenter(marker.getPosition());
        infoWindow.setContent(this.html);
        infoWindow.open(map, this);
    });
 
    marker.setMap(map);
    map.setCenter(latLng);
    map.setZoom(6);
}

Broadcasting messages in real-time

So far we have achieved one goal of our application, which is to save the message and render it on Google map. However, we have not written the code to broadcast messages in real-time so that messages are visible to other clients. To do that, we will use Pusher. Before we write code, please do the following prerequisites:

  1. Sign up for Pusher account : Go to http://pusher.com/ and create a new account.

  2. Create a new Pusher app : Login to your pusher account and create a new app as shown below.You just need to give a name to the app.

MessageOnMap application

Make code changes to MessagRestService

After we have a Pusher account and created an app, we need to make changes to our REST service endpoint to trigger a push message using Pusher Java client. The Pusher Java client is just a wrapper around the Pusher REST service. After inserting the message into MongoDB, we publish a message which will be received by all the clients subscribed to a channel named "messages" and event "new_msg".

 @POST
    @Consumes(value = MediaType.APPLICATION_JSON)
    public void submit(Message message) throws Exception {
        DBCollection messages = db.getCollection("messages");
        BasicDBObject object = new BasicDBObject();
        object.put("message", message.getMessage());
        object.put("location", message.getLocation());
        messages.insert(object);
 
        ObjectMapper objectMapper = new ObjectMapper();
        String msgJson = objectMapper.writeValueAsString(message);
        Pusher.triggerPush("messages", "new_msg", msgJson);
    }

Create Pusher environment variables

The Pusher client that we used above requires three environment variables corresponding to the Pusher app id, the Pusher app key , and the Pusher app secret. When you create the application, Pusher creates these three values for your application. You can view these values in the Pusher web console.

Create a new bash script called "pre_start_jbossas-7" in .openshift/action_hooks folder and add the lines shown below. Make sure this file is executable.

export PUSHER_APP_ID=xxx
export PUSHER_APP_KEY=xxx
export PUSHER_APP_SECRET=xxx

Replace xxx with correct values.

Include the Pusher JavaScript library

To enable clients to listen for messages, we need to add the pusher javascript file in index.html.

<script src="https://d3dy5gmtp8yhk7.cloudfront.net/2.0/pusher.min.js"></script>

Connect to Pusher

I’ve added some logging here which is always handy while developing applications. I then connect to Pusher by creating a new Pusher instance and passing it PUSHER_APP_KEY. Then a subscription is made to the messages channel.

// Log debug information to the JavaScript console, if possible
        Pusher.log = function(msg) {
            if (window.console && window.console.log) {
                window.console.log(msg);
            }
            $('#debug').prepend("  " + msg + "\n");
        };
 
        // Create new Pusher instance and connect
        var pusher = new Pusher(PUSHER_APP_KEY);
 
        // Subscribe to the channel that the event will be published on
        var channel = pusher.subscribe('messages');
 
        // Bind to the event on the channel and handle the event when triggered
        channel.bind('new_msg', function(data){
            renderMessageOnMap(data);
        });

Deploy the application

Finally you can deploy the application to OpenShift by adding and committing all the code to your local git repository and then pushing the changes to your OpenShift remote repository.

$ git add .
$ git commit -am "MessageOnMap application created"
$ git push

Conclusion

This blog showcased how you can create realtime location aware applications and deploy them to OpenShift. OpenShift and its partner ecosystem provides developers the services required to build highly scalable applications.

What's Next?