Web App using Java, Aerospike, Play and Scala

Recently I was exposed to Play Framework, by one of my favorite evangelists James Ward during his session at OSCON, and it looked cool so I decided to take the framework for a spin. The end result — a simple To-Do web app built in JASPS as in Java, Aerospike, Play and Scala.

Note: Working knowledge of Java, Scala, Play Framework and MVC pattern for writing web applications is expected.

Alright, let’s get to it!

The application itself is simple (as if that needs clarification in case of a To-Do app!) and does not necessarily solve a “problem” but the purpose of it is to show how you may use these technologies to build an app. One other piece of technology I’ve used is Activator which helps in building reactive apps. I also want to point out that the app is coded to be a single-user app — in order to keep it dead simple and focus more on technologies used.

Ok, where’s the code?

The entire working solution as well as instructions on how to quickly get the app up and running are available on GitHub

So why this post?

Well, I want to highlight a few things that may not be obvious just by looking at the code. For instance, opening and closing connection to the Aerospike Cluster only once when the app starts and terminates respectively. I have accomplished this by creating a Play Plugin and overriding onStart() and onStop() methods of Plugin.

Code

ASPlugin.java

Let’s examine the Play Plugin code located in the application root folder. This is where connection to the Aerospike Cluster is established when application starts and closed when application terminates.

package plugins;

import play.Application;
import play.Configuration;
import play.Logger;
import play.Plugin;
import play.api.Play;
import com.aerospike.client.*;

public class ASPlugin extends Plugin
{
  private final Application application;
  private static AerospikeClient ASClient; 

  public ASPlugin(Application application)
  {
    this.application = application;
  }

  @Override
  public void onStart()
  {
    Configuration configuration = application.configuration();

    //  Read Aerospike cluster config parameters from application.conf 
    String asClusterIP = configuration.getString("aerospike.cluster.ip");
    int asClusterPort = Integer.parseInt(configuration.getString("aerospike.cluster.port"));

    try 
    {
      //  Establish Aerospike cluster connection
      ASClient = new AerospikeClient(asClusterIP, asClusterPort);
      Logger.debug("Connection to Aerospike cluster established!");
    } catch (AerospikeException e) {
      Logger.debug("Connection to Aerospike cluster failed!");
      Logger.debug("AerospikeException: " + e.toString());
    } catch (Exception e) {
      Logger.debug("Connection to Aerospike cluster failed!");
      Logger.debug("Exception: " + e.toString());
    }

    Logger.info("ASPlugin started");
  }

  @Override
  public void onStop()
  {
    if (ASClient != null && ASClient.isConnected()) {
      //  Close Aerospike cluster connection
      Logger.debug("Connection to Aerospike cluster closed!");
      ASClient.close();
    }
    Logger.info("ASPlugin stopped");
  }

  public static AerospikeClient getASClient() {
    return ASClient;
  }
}
Aerospike Java Client
  • It is bundled with the app in /lib folder and imported up top

import com.aerospike.client.*;

onStart()
  • The Aerospike Cluster IP and Port settings are read from application.conf file in these lines of code

String asClusterIP = configuration.getString(“aerospike.cluster.ip”); int asClusterPort = Integer.parseInt(configuration.getString(“aerospike.cluster.port”));

  • A private instance of Aerospike Client is created — this effectively establishes connection to the Aerospike Cluster

ASClient = new AerospikeClient(asClusterIP, asClusterPort);

getASClient()
  • The Aerospike Client instance created in onStart() is made available via this getter

public static AerospikeClient getASClient() { return ASClient; }

onStop()
  • If the connection to Aerospike Cluster is open, it is closed here

ASClient.close();

Application.java

Let’s examine the controller code located in the app/controllers folder. This is where the actions for displaying, creating and deleting to-dos are defined. The common theme among these actions is they all check if connection to the Aerospike Cluster is established. If not, an error is displayed.

package controllers;

import play.*;
import play.mvc.*;
import views.html.*;
import com.aerospike.client.*;
import java.util.*;
import plugins.*;

public class Application extends Controller {

    private static final String defaultNamespace = "test";
    private static final String todoSet = "todos";
    private static final String userSet = "users";
    private static final AerospikeClient client = Play.application().plugin(ASPlugin.class).getASClient();

    public static Result index() {
        if (!client.isConnected())  {
            return ok(index.render(false,"Connection to Aerospike cluster failed! Please check your Aerospike IP and Port settings in application.conf",null));
        }
        try {
            Map<String, String> todos = new HashMap<String, String>();

            // Get how many todos the user has; NOTE: for simplicity, this is a single-user app so the user id is hardcoded to 'newuser'
            Key userKey = new Key(defaultNamespace, userSet, "newuser");
            Record userRecord = client.get(null, userKey);

            if (userRecord != null) {                   
                int todoCount = (Integer) userRecord.getValue("todocount");

                if (todoCount > 0)  {
                    //  Retrieve existing to-dos using batch operation

                    //  Create an array of keys so we can initiate batch read operation
                    Key[] keys = new Key[todoCount];
                    for (int i = 0; i < keys.length; i++) {
                        keys[i] = new Key(defaultNamespace, todoSet, ("newuser:" + (i + 1)));
                    }

                    // Initiate batch read operation
                    Record[] records = client.get(null, keys);
                    for (int j = 0; j < records.length; j++) {
                        if (records[j] != null) {
                            todos.put(records[j].getValue("k").toString(),records[j].getValue("todo").toString());
                        }
                    }
                } 
            } else  {
                //  Create user record and set default todo count to 0
                Bin todoCountBin = new Bin("todocount", 0);
                client.put(null, userKey, todoCountBin);
            }
            return ok(index.render(true,"ok",todos));
        } catch (AerospikeException e) {
            return ok(index.render(false,"AerospikeException: " + e.toString(),null));
        } catch (Exception e) {
            return ok(index.render(false,"Exception: " + e.toString(),null));
        } 
    }

    public static Result createTodo() {
        if (!client.isConnected())  {
            return ok(index.render(false,"Connection to Aerospike cluster failed! Please check your Aerospike IP and Port settings in application.conf",null));
        }
        try {
            //  Read To-Do entered in the form
            Map<String, String[]> values = request().body().asFormUrlEncoded();
            String todo = values.get("todo")[0];

            //  Get how many todos the user has; NOTE: for simplicity, this is a single-user app so the user id is hardcoded to 'newuser'
            Key userKey = new Key(defaultNamespace, userSet, "newuser");
            Record userRecord = client.get(null, userKey);
            int todoCount = (Integer) userRecord.getValue("todocount");

            //  Create new To-Do
            //  NOTE: the todoId is in format <newuser>:# so that we can initiate batch read by iterating over todo keys from 0 to user's <tweetcount> which is stored in the user record
            String todoId = ("newuser:" + (todoCount + 1));
            Key todoKey = new Key(defaultNamespace, todoSet, todoId);
            Bin todoKeyBin = new Bin("k", todoId);
            Bin todoBin = new Bin("todo", todo);
            Bin todoTSBin = new Bin("ts", System.currentTimeMillis());
            client.put(null, todoKey, todoKeyBin, todoBin, todoTSBin);

            //  Increment todocount
            client.put(null, userKey, new Bin("todocount", todoCount + 1));     
        } catch (AerospikeException e) {
          return ok(index.render(false,"AerospikeException: " + e.toString(),null));
        } catch (Exception e) {
          return ok(index.render(false,"Exception: " + e.toString(),null));
        } 
        //  Redirect the user to home/index/default route which will automatically refresh/reload the To-Dos
        return redirect("/");
    }

    public static Result deleteTodo() {
        if (!client.isConnected())  {
            return ok(index.render(false,"Connection to Aerospike cluster failed! Please check your Aerospike IP and Port settings in application.conf",null));
        }
        try {
            //  Read To-Do key passed in the form as hidden field
            Map<String, String[]> values = request().body().asFormUrlEncoded();
            String todoId = values.get("todoKey")[0];

            //  Get how many todos the user has; NOTE: for simplicity, this is a single-user app so the user key is hardcoded
            Key userKey = new Key(defaultNamespace, userSet, "newuser");
            Record userRecord = client.get(null, userKey);
            int todoCount = (Integer) userRecord.getValue("todocount");

            //  Delete To-Do
            Key todoKey = new Key(defaultNamespace, todoSet, todoId);
            client.delete(null, todoKey);
        } catch (AerospikeException e) {
          return ok(index.render(false,"AerospikeException: " + e.toString(),null));
        } catch (Exception e) {
          return ok(index.render(false,"Exception: " + e.toString(),null));
        } 
        //  Redirect the user to home/index/default route which will automatically refresh/reload the To-Dos
        return redirect("/");
    }

}

Before we look at controller actions, note this line of code

private static final AerospikeClient client = Play.application().plugin(ASPlugin.class).getASClient();

This is where we get reference to the Aerospike Client instance created when the application starts, as described above in ASPlugin.java. This instance is used to write, read and delete records.

index()
  • This controller action gets executed when the page is loaded
  • The first thing done here is a check to see if the (only) user with id ‘newuser’ exists. If not, it gets created using Aerospike’s put API
  • If user record exists, to-dos are retrieved using Aerospike’s get API in batch mode — this is done by passing in an array of keys to get
  • Retrieved to-dos are populated in a Map and passed into index.scala.html for rendering
createTodo()
  • This controller action gets executed when form is submitted by clicking on Add button
  • The to-do is created using Aerospike’s put API
  • Note that the todoId for new to-dos is in format newuser:# so that we can initiate batch read by iterating over todo keys from 0 to user’s tweetcount which is stored in the user record
  • Once the to-do is created, the user is redirected to the root/home route which executes the index() action — effectively refreshing the list of to-dos … not the most efficient way to refresh in a production app but it’s done here in that manner for simplicity. Ideally you’d just updated the model that holds to-dos and let two-way data binding do it’s magic to update the list.
deleteTodo()
  • This controller action gets executed when form to delete is submitted by clicking on X next to a to-do
  • The to-do is deleted using Aerospike’s delete API
  • Once the to-do is deleted, the user is redirected to the root/home route which executes the index() action — effectively refreshing the list of to-dos … not the most efficient way to refresh in a production app but it’s done here in that manner for simplicity. Ideally you’d just updated the model that holds to-dos and let two-way data binding do it’s magic to update the list.

Views

  • The two views are defined in app/views folder.
  • main.scala.html is the main view template within which all other views are rendered/yielded
  • index.scala.html has the markup to display, create and delete to-dos

Routes

  • The routes are defined in conf/routes file

If All Goes Well

  • This is what it looks like

Ok, so where’s the code again?

The entire working solution as well as instructions on how to quickly get the app up and running are available on GitHub

Leave a Reply