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 toget
- 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 formatnewuser:#
so that we can initiate batch read by iterating over todo keys from 0 to user’stweetcount
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/yieldedindex.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