On almost every topic
jQuery DataTables and Spring Web Scripts

This tutorial, the third covering jQuery DataTables and Alfresco, focuses on writing your own back-end services. After working with Alfresco’s OpenSearch interface and CMIS services, we will create a Web Script enabling the user to search for content and display the results in a table. You can find references to the other two related posts at the end of this tutorial.

Download the sample code

You can download the sample code here.

Goals

The goal of this tutorial is to demonstrate how easy it is to write your own services using Spring Web Scripts. A Web Script is a service bound to a URI which responds to HTTP methods such as GET or POST.

Spring Web Scripts

Spring Web Scripts is part of the Spring Surf extension project and provides an implementation of the Model View Controller (MVC) pattern, a well known design pattern used by web developers to design user interfaces. 

In this pattern the model represents the non-visual objects that contain the information we want to show, the view contains the presentation of this information in the user interface and the controller handles all the changes we make to the information. With Web Scripts you can implement this pattern using light weight scripting technologies like JavaScript for the controller and Freemarker for the view. You can also implement the controller using Java.

The Web Script Framework was originally developed by Alfresco, but it was donated to Spring Source because developing web frameworks is not Alfresco’s core business.

Create the Web Script

We will start with writing the backend service. To create a Web Script you first decide what URI to use for your service and what parameters are required in order to execute the behaviour that sits behind the URI. As an example we create a service that can be accessed using the following URI:

http://localhost:8080/service/com/someco/simplesearch

This service requires no parameters, but it will only return results when the user provides a search parameter. Two additional parameters are used for the page size and the skip count.

We can specify our service using a descriptor file like this:

<webscript>
  <shortname>Simple Search</shortname> 
  <description>Simple Search Example</description>
  <url>/com/someco/simplesearch</url>
  <authentication>user</authentication>
</webscript>

This script will simply forward any parameters to the controller. It is recommended to specify the required and optional parameters using a URI Template, but for now we will leave it this way.

Save this file as simplesearch.get.desc.xml in the following directory of your Alfresco distribution:

tomcat/shared/classes/alfresco/templates/webscripts/com/someco/sample

Add the controller

Now that we defined our service, we can write the controller. Create a file simplesearch.get.js in the same folder where we saved the descriptor file and add the following lines:

var iDisplayLength = 10;
var iDisplayStart = 0;

if (args.iDisplayLength != undefined && args.iDisplayLength.length > 0) 
{
  iDisplayLength = Number(args.iDisplayLength);
}

if (args.iDisplayStart != undefined && args.iDisplayStart.length > 0) 
{
  iDisplayStart = Number(args.iDisplayStart);
}

var nodes = new Array();

if (args.sSearch != undefined && args.sSearch.length > 0) 
{
  var result = search.luceneSearch("TYPE:\"cm:content\" AND TEXT:\"" + args.sSearch + "\"");

  var iTotalRecords = result.length;
 
  var totalPageItems = Math.min(iDisplayLength, iTotalRecords - iDisplayStart);

  for (i = 0; i < totalPageItems; i++){
    nodes.push(result[iDisplayStart+i]);
  }
}

model.aaData = nodes;
model.iTotalRecords = iTotalRecords;

This script basically checks the request parameters for the number of items to return per page and where to start (the skipcount) and if there is a search term provided it executes a search for content items that contain the search terms.

To implement paging raises an issue with Alfresco. Alfresco’s JavaScript Search Service supports paging, but it will not return the total number of items. Alfresco uses Lucene and Lucene does not provide a count function either.

So to support paging the total number of records is first retrieved and then the number of items to actually retrieve are calculated using the iDisplayLength, the iTotalRecords and iDisplayStart variables. The items are then retrieved from the result set and added to an Array:

var result = search.luceneSearch("TYPE:\"cm:content\" AND TEXT:\"" + args.sSearch + "\"");

var iTotalRecords = result.length;
 
var totalPageItems = Math.min(iDisplayLength, iTotalRecords - iDisplayStart);

for (i = 0; i < totalPageItems; i++){
  nodes.push(result[iDisplayStart+i]);
}

Finally the array containing the nodes and the total number of items are added to the model:

model.aaData = nodes;
model.iTotalRecords = iTotalRecords;

This model is passed to the view in order to create the response.

Add the view

Since we write our own service we can allow our service to return a data format that is fully supported by jQuery DataTables. First create a file called simplesearch.get.json.ftl. Open this file and add the following lines:

<#escape x as jsonUtils.encodeJSONString(x)>
{
  "sSearch": "${sSearch!""}", 
  "iTotalRecords": ${iTotalRecords}, 
  "iTotalDisplayRecords": ${iTotalRecords},
  "aaData": [
  <#list aaData as child>
	[
	"${child.name}",
	"${child.properties["cm:creator"]}",
	"${child.properties["cm:created"]?string("yyyy-MM-dd")}"
	]<#if child_has_next>,</#if></#list>
  ] 
}
</#escape>

This template will create a response like this:

{
  "iTotalRecords": 2, 
  "iTotalDisplayRecords": 2,
  "aaData": [
    ["example test script.js","System","2011-03-17"],	
    ["create-test-content.js","admin","2011-03-20"]
  ] 
}

Reload the Web Scripts Registry

Before we can use this new service we need to reload the Web Scripts Registry in Alfresco. Login to Alfresco as administrator and then go to the following URL:

http://localhost:8080/alfresco/service/

At the bottom of this page you should see a button with the text Refresh Web Scripts. Click this button to refresh. Once Alfresco is finished you should see a message that maintenance is completed. In addition you should see that Alfresco added a Web Script to the Web Script Registry:

Reset Web Scripts Registry; registered 381 Web Scripts. Previously, there were 380.

You can browse to the page for the web Script using Browse by Web Script Package and then by clicking the com/someco/sample package. You should see a page simlar to this:

By clicking the link behind the id Alfresco will return a page with all the details for the Web Script including the descriptor and the code for the controller and the view.

We can now test our Web Script by visiting the following URL:

http://localhost:8080/alfresco/service/com/someco/simplesearch

This request should return a JSON Array.

Debugging Web Scripts

You can change some settings in the log4.properties file to enable debugging for the JavaScript controller. Open the file in a text editor (it is located in the WEB-INF/classes directory of the Alfresco web application) and look for the following lines:

# Web Framework
log4j.logger.org.springframework.extensions.webscripts=info
log4j.logger.org.springframework.extensions.webscripts.ScriptLogger=warn
log4j.logger.org.springframework.extensions.webscripts.ScriptDebugger=off

You can set the first two lines to debug and the debugger to on. When you add log statements to the controller code, the statements will be written to the log file. For example:

logger.log("Total records: " + iTotalRecords);

When you use the script debugger, Alfresco will open a debugger once you execute a Web Script. The debugger allows you to step through the code at run-time and it enables access to all the objects loaded by the script.

Create the client page

The final step is to create the client page. I used the same ROOT folder in the webapps folder for my Alfresco distribution as I used for the other two posts about DataTables and Alfresco. Create a file simplesearch.html in the ROOT folder. This folder should also contain the jQuery DataTables libraries. See the post jQuery DataTables and Alfresco for more details. Next add the following lines to the file:

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>jQuery DataTables Simple Search</title>
    <style type="text/css" title="currentStyle">
      @import "media/css/demo_page.css";
      @import "media/css/demo_table.css";
    </style>
    <script type="text/javascript" src="media/js/jquery.js"></script>
    <script type="text/javascript" src="media/js/jquery.dataTables.js"></script>
    <script type="text/javascript" src="media/js/jquery.setFilteringDelay.js"></script>			
    <script type="text/javascript" charset="utf-8">
      $(document).ready(function() {
        $('#example').dataTable( {
          "bServerSide": true,
          "bPaginate": true,
          "sPaginationType": "full_numbers",
          "sAjaxSource": "http://localhost:8080/alfresco/s/com/someco/simplesearch",
          "fnInitComplete": function(){this.fnSetFilteringDelay(500)},
          "fnServerData": function ( sSource, aoData, fnCallback ) {
            $.getJSON( sSource, aoData, function (json) { 
              fnCallback(json);
            } );
          },
          "bFilter": true,
          "bSort": false,
          "bInfo": true
        } );
      } );
    </script>
  </head>
  <body id="dt_example">
    <div id="container">
      <div id="dynamic">
        <table cellpadding="0" cellspacing="0" border="0" class="display" id="example">
          <thead>
            <tr>
              <th width="60%">Name</th>
              <th width="20%">Creator</th>
              <th width="20%">Modified</th>
            </tr>
          </thead>
        <tbody>
          <tr>
            <td colspan="7" class="dataTables_empty">Loading data from server</td>
          </tr>
        </tbody>
      </table>
    </div>
  </body>
</html>

Since our service returns a JSON Array that is compatible with the data format that DataTables uses, there is no need to post process the response to create the JSON Array like we had to do in the previous posts. If you load this page in your web browser you should see a similar response page:

Update the view

We can now update the view, for example to add the actual name of the author as a replacement for the creator. Since the information is already available in the model, the only thing you need to do is to revisit the Freemarker template that creates the JSON reponse:

<#escape x as jsonUtils.encodeJSONString(x)>
{
  "sSearch": "${sSearch!""}", 
  "iTotalRecords": ${iTotalRecords}, 
  "iTotalDisplayRecords": ${iTotalRecords},
  "aaData": [
  <#list aaData as child>
	[
	"${child.name}",
	"${child.properties["cm:author"]!${child.properties["cm:creator"]}",
	"${child.properties["cm:created"]?string("yyyy-MM-dd")}"
	]<#if child_has_next>,</#if></#list>
  ] 
}
</#escape>

When you only update the view, there is no need to refresh the Web Scripts Registry. Simply refreshing the page should update our table with proper author names.

Add a column

To add a column we need to add a field to the view simplesearch.get.json.ftl and a column to the table in simplesearch.html. First add the following line to the Freemarker template right under the name property:

"${child.properties["cm:title"]!""}",

Next add the following table heading to the table definition in the HTML file:

<thead>
  <tr>
    <th width="30%">Name</th>
    <th width="30%">Title</th>
    <th width="20%">Creator</th>
    <th width="20%">Modified</th>
  </tr>
</thead>

Refresh the page and you should see the column added to the table:

Add sorting

Now when you set the bSort parameter in the table inizialization code to true and reload the client page, you can see that there are a couple of parameters added to the request including a parameter called iSortCol_0 that provides the column number for the first field to sort by (0). When you click the author column for example the value for the iSortCol_0 request parameter will be 1.

In addition to the sort column parameter, the request will also contain a parameter sSortDir_0 containing the sort direction.

Our JavaScript controller will receive the parameter, but since we did not add sorting to our query, the order in which the results appear will not change. To do this we need to update the controller code.

We receive the column position and not a field name, so we first add an Array with a mapping of the column numbers and the corresponding field name. Add the following at the top of the controller script:

var sortColumns = new Array();
sortColumns[0] = "@{http://www.alfresco.org/model/content/1.0}name"; 
sortColumns[1] = "@{http://www.alfresco.org/model/content/1.0}title";
sortColumns[2] = "@{http://www.alfresco.org/model/content/1.0}author";
sortColumns[3] = "@{http://www.alfresco.org/model/content/1.0}modified";

We then add the declaration for the parameters required for sorting. Add the declarations for example right under the declaration for iTotalRecords:

var sortColumn = sortColumns[0];
var sortAscending = true;

We can then set the parameters for the sorting column and the sorting direction:

if (args.iSortCol_0 != undefined && args.iSortCol_0.length > 0 && args.iSortCol_0 <= sortColumns.length) 
{
  sortColumn = sortColumns[args.iSortCol_0];
}

if (args.sSortDir_0 != undefined && args.sSortDir_0.length > 0) 
{
  if (args.sSortDir_0 == "desc")
  {
    sortAscending = false;
  }
}

We now have all the information needed to execute a query that sorts. To do this we use the query method of the JavaScript Search API. It provides extensible configuration options using a query definition. Replace the line:

var result = search.luceneSearch("TYPE:\"cm:content\" AND TEXT:\"" + args.sSearch + "\"");

With the query using the query definition:

var def =
{
  query: "TYPE:\"cm:content\" AND TEXT:\"" + args.sSearch + "\"",
  sort: [{column: sortColumn, ascending: sortAscending}]
};

var result = search.query(def);

Since the controller is updated we need to refresh the Web Scripts Registry to make the changes active. After refreshing the Web Scripts Registry you should be able to click the table headings to change the sorting column and sorting order:

Conclusion

In this post I returned to the jQuery DataTables and Alfresco to show how you can write your own back-end Spring Web Script to populate a DataTable. Web Scripts provide a simple MVC model to develop services using light-weight scripting techniques.

Read the previous tutorials

Read the related two posts jQuery DataTables, CMIS and Alfresco and jQuery DataTables and Alfresco. They cover jQuery DataTables with CMIS services and Alfresco’s OpenSearch keyword search as a back-end service.

Updates

  • March 30, 2011: changed package name to conform to upcoming post about Java based Web Script.
  1. psdtohtmlshop reblogged this from bpeters
  2. xn----slbefaduec3bfcs8cycfbfdiq reblogged this from bpeters
  3. bpeters posted this