My first Play Framework application has recently gone live. This time it was the Java version of the framework, next time I may move on to Scala finally. Nevertheless, I've learned a lot and will try to share some knowledge here. The application is meant for managing the data stored in the Dydra graph database (RDF & SPARQL). It's a thick client application meaning that the data loading happens in the client JavaScript layer via JSON requests, while routing and user authentication is done by means of Play Framework. Also I've made use of the RequireJS support in Play Framework for dynamic JS module loading. For the UI I've chosen a well-known YUI library. More details are following below.
- Application architecture.
- User model and authentication.
- Dydra database layer and SPARQL client.
- JavaScript logic and YUI.
- RequireJS module loading.
As any web application, this one can be described in terms of MVC pattern. Play Framework encourages following this pattern by introducing Ebean ORM with support for JPA annotations, Model/Controller class hierarchies and the powerful Scala-based template engine. Having played enough with Spring MVC's request mapping annotations, I've found it awesome that Play Framework introduces the central configuration file for all the HTTP routing in the application - conf/routes file.
This was a generic Play Framework-based application design. However, my application deviates with most of the controller logic written in JavaScript, while only the user authentication is implemented with the default approach. BTW, for this purpose Play Framework provides security helpers with Java annotations that you'll see below.
This was a generic Play Framework-based application design. However, my application deviates with most of the controller logic written in JavaScript, while only the user authentication is implemented with the default approach. BTW, for this purpose Play Framework provides security helpers with Java annotations that you'll see below.
User model and authentication
I've borrowed the implementation of the user model and authentication from the sample application Zentasks. That official sample application tutorial is a great way for a quick start. Then I've customized it for my case. For example, I've added Unsecured authenticator to allow anonymous users to preview the home page without redirection to the unauthorized page.
public class Unsecured extends Security.Authenticator { @Override public String getUsername(Context ctx) { String name = ctx.session().get("name"); // Returns "" to avoid redirection to unauthorized page return name == null ? "" : name; } }
Then the home page controller class looks like:
@Security.Authenticated(Unsecured.class) public class Home extends Controller { // Action handlers here }
Finally, I've benefited from the database evolutions support. They allow managing database changes in a simple way. Moreover, you can configure the evolutions to be applied automatically without pressing "Apply this script now!" button. So my database configuration looks like:
# Using default H2 database in the embedded mode db.default.driver=org.h2.Driver db.default.url="jdbc:h2:db/play" # Database evolutions are applied automatically applyEvolutions.default=true
Dydra database layer and SPARQL client
Main functional part of the application's model is based on Dydra RDF database. Although there already exists a JPA-like Play Framework plugin for RDF databases called Imperium, I've decided it's an overkill for my use case and have opted for the client-side JavaScript SPARQL client. For this purpose I've borrowed sparql-client.js implementation (a part of SKOSjs application).
So I've ended up using cross-domain JavaScript requests to interact with the database. By default their usage is forbidden due to security reasons, so the browsers will complain. However, as long as the server sends a Access-Control-Allow-Origin response header (that is done by Dydra database), such requests will work with some restrictions. First, I could not make HTTP POST requests working cross-domain, so I used HTTP GET for SPARQL 1.1 Update queries. Second, although there are several authentication ways in Dydra, I've ended up using "API Authentication Key as a Query String Parameter".
So I've ended up using cross-domain JavaScript requests to interact with the database. By default their usage is forbidden due to security reasons, so the browsers will complain. However, as long as the server sends a Access-Control-Allow-Origin response header (that is done by Dydra database), such requests will work with some restrictions. First, I could not make HTTP POST requests working cross-domain, so I used HTTP GET for SPARQL 1.1 Update queries. Second, although there are several authentication ways in Dydra, I've ended up using "API Authentication Key as a Query String Parameter".
JavaScript logic and YUI
I've reused the YUI3-based Transaction utility again and have built a number of child transaction classes that encapsulate sending SPARQL requests to the RDF database and rendering the results to the user. Here I'll show just a basic SparqlTransaction that builds the SPARQL query from the template and parameters, then sends the SPARQL request to the RDF database and returns the resulting JSON data for further processing. Afterwards, the data can be represented in a YUI data table or in any other way. However, that code still requires a lot of refactoring so I may show some examples later in another post.
// Basic SPARQL transaction extends ContainerTransaction function SparqlTransaction(config) { Y = config.Y; Y_transaction.ContainerTransaction.call(this, config); // this function is called when the SPARQL request returns some data // to be overriden this.onDataLoad = function(config, data) {}; // override the onComplete function this.onComplete = function (txId, response) { // response already contains SPARQL query // but it needs to be parametrized var query = formatQuery(response.responseText, config.queryParams); // sending SPARQL query HTTP GET request with success/failure callbacks config.client.select(query, function (data, caller) { hideLoadingImage(config.containerId); caller.onDataLoad(config, data); }, function () { hideLoadingImage(config.containerId); alert("Connectivity issue occurred") }, this); }; function hideLoadingImage(containerId) { if (containerId) Y.one("#" + containerId).get('childNodes').remove(); } } // Substitutes the placeholders {n} with corresponding parameters function formatQuery() { var queryString = arguments[0]; var parameters = arguments[1] || []; for (var i = 0; i < parameters.length; i++) { queryString = queryString.replace( new RegExp('\\{'+i+'\\}', 'g'), parameters[i]); } return queryString; }
Here is how I've created and invoked the SPARQL transaction to show some response to the user:
var transaction = new Common.SparqlTransaction({ "Y": Y, "uri": query_url, "queryParams": [id], "client": client }); transaction.onDataLoad = function(config, data) { var props = data.results.bindings[0]; // showing props values to the user here }; transaction.execute();
To conclude, the SPARQL transaction sends the AJAX request to the server to read the SPARQL query resource, then the SPARQL query is parametrized and sent to the RDF database using the SPARQL client. Finally, the JSON results of the SPARQL request are rendered to the user.
RequireJS module loading
Play Framework supports one more cool feature - JavaScript module loading based on RequireJS library. Here you can find hints and examples explaining how to make use of this feature. It can be extremely useful, especially in my case when most controller logic is written in JavaScript. However, I still have some issues to resolve as combining resources and minification do not seem to work properly yet. This is because I've made use of YUI and have tried to integrate it with RequireJS using UseYUI plugin. It will be a good topic for another post as soon as I resolve the issues.
Users praised the usability, real good job Ivan :)
ReplyDelete