On almost every topic
Alfresco Java Based Spring Web Script

This post is a follow-up to jQuery DataTables and Spring Web Scripts that focused on writing a back-end service using light weight scripting techniques. In this post we will replace the JavaScript controller with a controller written in Java. This post is just an example. In most cases you can and should use the JavaScript API. It is recommended to read the previous post before you continue.

Download the sample code

You can download the sample code here. The download contains an Eclipse project as a separate archive file. It requires Eclipse and the Alfresco SDK. You can also use a different editor if you want. The client-side code is also added in the ROOT folder. Add the ROOT folder to the webapps folder of your Alfresco Tomcat distribution and download and add the jQuery DataTables libraries to setup the client.

Import the project

To import the project start Eclipse, select File, Import and then Archive File. Select the archive file simple-search-eclipse.zip and click Finish to import the project. Once the project is imported make sure that you add the project SDK Embedded from the Alfresco SDK.

To do this download the SDK, unpack the files and then in Eclipse select File, Import, then choose Existing Projects into Workspace. In the Import dialog select the samples folder in the Alfresco SDK as root folder, select the SDK Embedded project and click Finish to import the project.

To add the SDK embedded project to the Eclipse project select Project in the menu bar, then Properties and select the Java Build Path. Click Add in the Projects tab to add the project.

What we need to do

In order to use a Java class as the controller for a Web Script, we need to write the Java source file and bind the class to our Web Script using a Spring bean declaration. The Spring bean declaration is an XML configuration file that configures the link between the Web Script and the Java class. 

We also need the Web Script descriptor file simplesearch.get.desc.xml and the view simplesearch.get.json.ftl. The file simplesearch.get.js is no longer needed. It contains the JavaScript controller that will be replaced with the Java class. We start with writing the Java code.

Add the Java class

In the source directory of your project create the following package:

com.someco.alfresco.web.scripts.bean

And add a class SimpleSearch.java as a subclass of:

org.springframework.extensions.webscripts.DeclarativeWebScript

You will end up with the following class definition:

package com.someco.alfresco.web.scripts.bean;

public class SimpleSearch extends DeclarativeWebScript {

}

In order to use Alfresco’s public services we add the ServiceRegistry. This registry provides access to all of Alfresco’s public services like the SearchService or the NodeService. You can also add the different services you need, but since we are required to provide the ServiceRegistry to a method later, we simply add the registry and use that to retrieve the services we need.

package com.someco.alfresco.web.scripts.bean;

public class SimpleSearch extends DeclarativeWebScript {

  protected ServiceRegistry serviceRegistry;

  @Override
  protected Map executeImpl(WebScriptRequest req,
    Status status) {

    return null;
  }

  public ServiceRegistry getServiceRegistry() {
    return serviceRegistry;
  }

  public void setServiceRegistry(ServiceRegistry serviceRegistry) {
    this.serviceRegistry = serviceRegistry;
  }

}

Tip: in Eclipse you can use Source, Generate Getters and Setters… to add the getter and setter methods for the serviceRegistry variable.

We start with setting the required parameters. Add the following lines to the executeImpl method:

String searchTerms = req.getParameter("sSearch");

String displayStartArg = req.getParameter("iDisplayStart");
int displayStart = 0;
try {
  displayStart = new Integer(displayStartArg);
} catch (NumberFormatException e) {
}

String displayLengthArg = req.getParameter("iDisplayLength");
int displayLength = 10;
try {
  displayLength = new Integer(displayLengthArg);
} catch (NumberFormatException e) {
}

return null;

Now that we have the required parameters, we can execute a search if the user provided a search term:

// we will use it to add the TemplateNode items
List list = new ArrayList();

int totalRecords = 0;

ResultSet results = null;

if (searchTerms != null && searchTerms.length() > 0) {

  try {
    // create the query statement
    StringBuilder query = new StringBuilder();
    query.append("TYPE:\"");
    query.append(ContentModel.TYPE_CONTENT);
    query.append("\" AND TEXT:\"");
    query.append(searchTerms);
    query.append("\"");

    // define search parameters
    SearchParameters parameters = new SearchParameters();
    parameters.addStore(Repository.getStoreRef());
    parameters.setLanguage(SearchService.LANGUAGE_LUCENE);
    parameters.setQuery(query.toString());

    // execute the query
    results = serviceRegistry.getSearchService().query(parameters);

    totalRecords = results.length();

    // get the number of items to retrieve for the current page
    int totalPageItems = Math.min(displayLength, totalRecords
        - displayStart);

    // add the nodes to the list for our model
    for (int i = 0; i < totalPageItems; i++) {
      NodeRef node = results.getNodeRef(i + displayStart);

      // the Freemarker model requires TemplateNode objects
      list.add(new TemplateNode(node, serviceRegistry, null));
    }

  } finally {
    // make sure that we close our result set to
    // avoid memory leaks
    if (results != null) {
      results.close();
    }
  }

}

And finally we build the model that will be passed to the view, our Freemarker template:

Map model = new HashMap(7, 1.0f);
model.put("iTotalRecords", totalRecords);
model.put("aaData", list);

return model;

The final Java class looks like this:

package com.someco.alfresco.web.scripts.bean;

public class SimpleSearch extends DeclarativeWebScript {

  protected ServiceRegistry serviceRegistry;

  @Override
  protected Map executeImpl(WebScriptRequest req,
      Status status) {

    String searchTerms = req.getParameter("sSearch");

    String displayStartArg = req.getParameter("iDisplayStart");
    int displayStart = 0;
    try {
      displayStart = new Integer(displayStartArg);
    } catch (NumberFormatException e) {
    }

    String displayLengthArg = req.getParameter("iDisplayLength");
    int displayLength = 10;
    try {
      displayLength = new Integer(displayLengthArg);
    } catch (NumberFormatException e) {
    }

    List list = new ArrayList();

    int totalRecords = 0;

    ResultSet results = null;

    if (searchTerms != null && searchTerms.length() > 0) {

      try {
        StringBuilder query = new StringBuilder();
        query.append("TYPE:\"");
        query.append(ContentModel.TYPE_CONTENT);
        query.append("\" AND TEXT:\"");
        query.append(searchTerms);
        query.append("\"");

        SearchParameters parameters = new SearchParameters();
        parameters.addStore(Repository.getStoreRef());
        parameters.setLanguage(SearchService.LANGUAGE_LUCENE);
        parameters.setQuery(query.toString());

        results = serviceRegistry.getSearchService().query(parameters);
        totalRecords = results.length();

        int totalPageItems = Math.min(displayLength, totalRecords
            - displayStart);

        for (int i = 0; i < totalPageItems; i++) {
          NodeRef node = results.getNodeRef(i + displayStart);
          list.add(new TemplateNode(node, serviceRegistry, null));
        }

      } finally {
        if (results != null) {
          results.close();
        }
      }

    }

    Map model = new HashMap(7, 1.0f);
    model.put("iTotalRecords", totalRecords);
    model.put("aaData", list);

    return model;
  }

  public ServiceRegistry getServiceRegistry() {
    return serviceRegistry;
  }

  public void setServiceRegistry(ServiceRegistry serviceRegistry) {
    this.serviceRegistry = serviceRegistry;
  }

}

I did not add all the import statements. In Eclipse you can organize imports using Source and then Organize Imports (Ctrl+Shift+O).

Add the Spring bean declaration

You can read more about the Spring bean declaration for a Java backed Web Script at Alfresco’s Wiki. Create a file called web-scripts-application-context.xml in the folder config/extension and add the following lines:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN 2.0//EN' 
  'http://www.springframework.org/dtd/spring-beans-2.0.dtd'>
<beans>
  <bean id="webscript.com.someco.samples.simplesearch.get" 
    class="com.someco.alfresco.web.scripts.bean.SimpleSearch"
    parent="webscript">
    <property name="ServiceRegistry" ref="ServiceRegistry" />
  </bean>
</beans>

What is important here is that the bean id follows the pattern below:

id="webscript.[packageId].[serviceId].[httpMethod]"

The packageId is the folder where the web script descriptor is located, the serviceId is the name of the service and the method is for example get or post. Also make sure that you set the parent bean to webscript.

Add the descriptor

Finally we need to add the descriptor and the view to the project. These files are the same files as we used for the previous Spring Web Scripts post with the JavaScript controller. First create the following folder in the project:

config/extension/templates/webscripts/com/someco/samples

Next create a file simplesearch.get.desc.xml and add the following lines to the file:

<webscript>
  <shortname>Simple Search</shortname>
  <description>Simple Search Example</description>
  <url>com/someco/simplesearch</url>
  <authentication>user</authentication>
  <format default="json">argument</format>	
</webscript>

Add the view

Then create a file simplesearch.get.json.ftl and add the following Freemarker code:

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

The last two files are no different from the descriptor and the view from the previous post about Spring Web Scripts. We are now ready to deploy the code.

Deployment

To be able to deploy this code check the build.properties file to make sure that the properties refer to your Alfresco distribution and to the Alfresco SDK. Then return to Eclpse, select Window and then Show View and select Ant. In the Ant View click the plus sign and add the build.xml from the Simplesearch project. Click deploy to deploy the code and restart Alfresco.

The Ant build tool writes the Java library simplesearch.jar to the WEB-INF/lib directory and the Web Script files to WEB-INF/classes/alfresco/extension. Make sure to remove the previous Spring Web Script to avoid a naming conflict.

Login to Alfresco and navigate to the Web Script maintenance page to check if the Simple Search service is a registered Web Script:

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

Reload the client page

When you reload the same client HTML file simplesearch.html as we used for the previous post located in the ROOT project, you should see the same result:

Java Backed Web Script 

The main difference is that we did not add sorting to our server-side code.

Add sorting

You can add sorting by adding the parameters for sorting to the class file like we did for the page length and skipcount. To map the column number to a property in Alfresco you can add a static Map.

In order to sort you need to provide a search parameter with the name of the field and a boolean value for ascending order (true) or descending order (false). The field name must be provided in the so called Clark notation preceded by an at sign (@):

@{http://www.alfresco.org/model/content/1.0}name

The following code snippet shows how you can add the sort order to the list of search parameters:

parameters.addSort("@" + ContentModel.PROP_NAME.toString(), true);

Conclusion

In this post I returned to Spring Web Scripts to show how you can replace a JavaScript controller with a Java based controller. In most cases you should be fine writing the controller using JavaScript, but in cases where you need to execute a task that is not easily done using JavaScript, you can use Java.

Read the previous tutorials

Read the related three posts jQuery DataTables and Spring Web Scripts, jQuery DataTables, CMIS and Alfresco and jQuery DataTables and Alfresco.

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.
jQuery DataTables, CMIS and Alfresco

This is a follow up to the post jQuery DataTables and Alfresco. In this tutorial we replace the OpenSearch service with a CMIS back-end service to retrieve content items from an Alfresco repository and populate a jQuery DataTable

Download the sample code

You can download the sample code here. The example code also provides an Alfresco JavaScript that can be used to create some content items. To do this you need to add the script to the Scripts space in the Data Dictionary and run the script using a Run Action.

About CMIS

CMIS (Content Management Interoperability Services) is SQL for content repositories. It allows access to different content management systems using an open standard maintained by OASIS. OASIS is an important consortium that develops and promotes open standards.

CMIS enables developers to create, update or delete documents or folders and to search for documents or folders using a vendor independent language. It is a very promising standard, especially since all major vendors of Enterprise Content Management software adopted the standard including Microsoft, Oracle, EMC Corporation (Documentum), IBM, SAP, Alfresco and Open Text. Alfresco was the first to provide full support for the standard.

For these examples we will use the Restful Atompub binding that uses XML as a data format. There is also an OASIS Subcommittee working on a browser binding to make it even easier to use CMIS in browser based applications. This binding will use JSON as a data format in stead of XML.

Prerequisites

This tutorial is a continuation of jQuery DataTables and Alfresco.

It might help to familiarize yourself with CMIS. For developers I can recommend the excellent CMIS introduction by Jeff Potts.

Create the file

The first step is to set up a basic HTML file for the client-side table. We will start with a simple table listing the name of the documents returned by the CMIS service. Create a file cmissearch.html in the ROOT folder that we used in the previous tutorial about DataTables and Alfresco:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>jQuery DataTables and Alfresco CMIS Example</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" src="media/js/jquery.base64.js"></script>
    <script type="text/javascript">
      <!-- here we will write our client-side JavaScript -->
    </script>
  </head>
  <body id="dt_example">
    <div id="container">
      <h1>jQuery DataTables and Alfresco CMIS Example</h1>
      <div id="dynamic">
        <table class="display" id="example">
	  <thead>
	    <tr>
	      <th width="30%">Name</th>
	    </tr>
	  </thead>
	  <tbody>
            <tr>
	      <td colspan="5" class="dataTables_empty">Loading data from server</td>
	    </tr>
	  </tbody>
	</table>
      </div>
    </div>
  </body>
</html>

As you might have noticed I added a new jQuery plug-in called jquery.base64.js. We need this plug-in to encode the username and password that we need to authenticate against the CMIS service. You can find it here.

Add the template

The next step is to add a template for the CMIS request. This is a bit of a hack, but it works. This approach was inspired by this post by developer Ben Nadel. Add the following script tag to the header of the HTML file:

<script id="cmis-template" type="application/cmis-template">
  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <cmis:query xmlns:cmis="http://docs.oasis-open.org/ns/cmis/core/200908/">
    <cmis:statement>SELECT cmis:name 
	  FROM cmis:document 
	  WHERE CONTAINS('${sSearch}') 
	  ORDER BY cmis:name</cmis:statement>
    <cmis:maxItems>${iDisplayLength}</cmis:maxItems>
    <cmis:skipCount>${iDisplayStart}</cmis:skipCount>
  </cmis:query>
</script>

I am going to use this template to prepare my requests to the server. It contains a query request that searches for text in content items and orders the result by name.

Initialize the DataTable

We will now setup the basic implementation of the client-side JavaScript. This part is only slightly different from the approach in the previous post about DataTables:

function fnGetJSONData( sSource, aoData, fnCallback ) {
   /* this function will execute the keyword search */
}
	
$(document).ready(function() {
  $('#example').dataTable( {
    "bServerSide": true,
    "sAjaxSource": "/alfresco/s/cmis/queries",
    "bSort": false,
    "bPaginate": true,
    "sPaginationType": "full_numbers",
    "fnInitComplete": function(){this.fnSetFilteringDelay(500)},
    "fnServerData": fnGetJSONData
  } );
} );

Implement the callback function

The final part is the implementation of the callback function. Here is the code:

function fnGetJSONData( sSource, aoData, fnCallback ) {

  var params = {};
  for ( var i=0 ; i var cmisTemplate = $( "#cmis-template" );
			
    var requestBody = cmisTemplate.html().replace('${sSearch}', params["sSearch"])
      .replace('${iDisplayStart}', params["iDisplayStart"])
      .replace('${iDisplayLength}', params["iDisplayLength"]);
			
    $.ajax( {
      "dataType": 'xml',
      "type": "POST", 
      "url": sSource, 
      "contentType" : "application/cmisquery+xml",
      "beforeSend" : function(req) {
        req.setRequestHeader("Authorization", "Basic " + $.base64Encode("admin:admin"));
      },
      "processData" : false,
      "data" : $.trim( requestBody ),
      "success": function (data,textStatus,xmlHttpRequest) {
				
        var jData = $( data );	

        var json = {"sEcho": params["sEcho"],"aaData" : []};
        json.iTotalRecords = jData.find("[nodeName='opensearch:totalResults']").text();
        json.iTotalDisplayRecords = json.iTotalRecords;
					
        var items = jData.find("entry").each(function(){
          json.aaData.push([
            $(this).find("title").text(),
          ]);
        });
           
        fnCallback(json);
      },
      error: function(){
        console.log( "ERROR", arguments );
      }
    } );					
  }
}

Even this part is not that different from what we did in the previous post. I have highlighted the major differences. 

The first step is to prepare the request body. In order to execute a CMIS query request, I created the template with id cmis-template. It contains a couple of interpolations. By using a global replace we can fill the template with our search properties:

var requestBody = cmisTemplate.html().replace('${sSearch}', params["sSearch"])
  .replace('${iDisplayStart}', params["iDisplayStart"])
  .replace('${iDisplayLength}', params["iDisplayLength"]);

The next step is the call to the backend. There are a couple of differences from the function used for the Open Search client. We need to post our data to the back-end server with a specific content type (application/cmisquery+xml) for CMIS and the service requires authentication. For now we will simply add a request header with a basic authentication:

"beforeSend" : function(req) {
  req.setRequestHeader("Authorization", "Basic " + $.base64Encode("admin:admin"));
},

When posting data we need to add the request to the request body. By setting the processData parameter to false, we can prevent the request method to process our request data. Otherwise it will try to convert our request body into request parameters.

I also applied the trim function to the request body to remove leading and trailing whitespace:

"data" : $.trim( requestBody ),

This is all we need to do in order to execute the request to the server.

In order to populate the table, we need to convert the XML response into a JSON array. This part is quite similar to the OpenSearch examples of the previous post about DataTables. We add the skipcount and the total number of records returned by the search and then iterate over the entries of the Atom feed to add a title for each content item retrieved by the query request:

var json = {"sEcho": params["sEcho"],"aaData" : []};
json.iTotalRecords = jData.find("[nodeName='opensearch:totalResults']").text();
json.iTotalDisplayRecords = json.iTotalRecords;
					
var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("title").text(),
  ]);
});

Now if we test this client page the result should be similar to this:

CMIS DataTables

Add columns

Once this is done we can easily extend our table to provide additional information. To add the name, the author and the modified date, for example, you only need to add the parameters to the JSON array:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("*[propertyDefinitionId='cmis:name']").find("[nodeName='cmis:value']").text(),
    $(this).find("title").text(),
    $(this).find("author").text(),
    $(this).find("updated").text()
  ]);
});

And the columns to the table:

<thead>
  <tr>
    <th width="30%">Name</th>
    <th width="30%">Title</th>
    <th width="20%">Author</th>
    <th width="20%">Updated</th>
  </tr>
</thead>

A tool like Firebug can be helpful here to analyze the response from the server to figure out how to retrieve the values from the XML returned by the CMIS service.

When you reload the page and execute a search you will now see a page similar to this:

CMIS DataTables

Retrieving aspects

This table does not display the values for the title and author that we entered as author and title in Alfresco. They are the name and created properties in stead. In order to show the desired author and title, we need to extend our query to retrieve the author and title that are part of the so called cm:titled and cm:author aspects.

Aspects are an important content modeling concept in Alfresco. They provide a more dynamic way to add metadata properties to documents and folders. For Alfresco developers it is a best practice to use aspects to create a content model. Even when you need content types it is recommended to create aspects for logical chunks of metadata and add them to a type definition as mandatory aspects. Aspects provide much more flexibility and make your content model easier to maintain. Aspects however are not recognized by CMIS as a concept, although that might change in a future version.

To add the properties from the aspects that contain the author and title information we need to add two joins to our query:

SELECT d.*, t.*, a.* 
  FROM cmis:document AS d 
  JOIN cm:titled AS t ON d.cmis:objectId = t.cmis:objectId 
  JOIN cm:author AS a ON d.cmis:objectId = a.cmis:objectId 
  WHERE CONTAINS(d,'${sSearch}') 
  ORDER BY d.cmis:name

Note: when you add aspects to your query using JOIN the search will only return folders or documents that have these aspects applied.

Additionally you can use IN_FOLDER() to match the immediate children of a folder or IN_TREE to match any object beneath the folder:

SELECT d.*, t.*, a.*
  FROM cmis:document AS d 
  JOIN cm:titled AS t ON d.cmis:objectId = t.cmis:objectId 
  JOIN cm:author AS a ON d.cmis:objectId = a.cmis:objectId 
  WHERE IN_FOLDER(d, 'workspace://SpacesStore/40312a4b-7767-4586-a58b-18d050ffe0ca')
  AND CONTAINS(d,'${sSearch}')
  ORDER BY d.cmis:name

To match docments that follow the file name pattern *.txt you can add a LIKE predicate to the WHERE clause for the cmis:name property:

SELECT d.*, t.*, a.*
  FROM cmis:document AS d 
  JOIN cm:titled AS t ON d.cmis:objectId = t.cmis:objectId 
  JOIN cm:author AS a ON d.cmis:objectId = a.cmis:objectId 
  WHERE IN_FOLDER(d, 'workspace://SpacesStore/40312a4b-7767-4586-a58b-18d050ffe0ca') 
  AND CONTAINS(d,'${sSearch}')
  AND d.cmis:name LIKE '%.txt'
  ORDER BY d.cmis:name

Once we added the aspects, we can retrieve the actual values for the author and the title using the following code:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("*[propertyDefinitionId='cmis:name']").find("[nodeName='cmis:value']").text(),
    $(this).find("*[propertyDefinitionId='cm:title']").find("[nodeName='cmis:value']").text(),
    $(this).find("*[propertyDefinitionId='cm:author']").find("[nodeName='cmis:value']").text(),
    $(this).find("updated").text()
  ]);
});

When we reload this page we will see the actual author and title properties:

CMIS Search 3

Add some formatting

Finally we will add some formatting to our table by adding an icon to the name column and by formatting the date. In order to format the date I added a custom jQuery UI library with only the core component and the jQuery UI Datepicker. The datepicker contains a date parser and formatter that is able to process a date in ISO8601 format as used by the Atom publishing protocol. Add the jQuery UI library to the header of the page:

<script type="text/javascript" src="media/js/jquery-ui-1.8.11.custom.min.js"></script>

I added the reference to the icon to the JSON array:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("[nodeName='alf:icon']").text(),
    $(this).find("*[propertyDefinitionId='cmis:name']").find("[nodeName='cmis:value']").text(),
    $(this).find("*[propertyDefinitionId='cm:title']").find("[nodeName='cmis:value']").text(),
    $(this).find("*[propertyDefinitionId='cm:author']").find("[nodeName='cmis:value']").text(),
    $(this).find("updated").text()
  ]);
});

And the column for the icon to the table:

<thead>
  <tr>
    <th width="0%">Icon</th>
    <th width="30%">Name</th>
    <th width="30%">Title</th>
    <th width="20%">Author</th>
    <th width="20%">Updated</th>
  </tr>
</thead>

And finally I added the column rendering to the table initialization code after the sPaginationType parameter:

"aoColumns": [
  {"bVisible": false},
  { "fnRender": function ( oObj ) {
    return '<img src="' + oObj.aData[0] + '" alt="Icon" /> ' + oObj.aData[1];
  } },
  null,
  null,
  { "fnRender": function ( oObj ) {
    var updated = $.datepicker.parseDate($.datepicker.ATOM, oObj.aData[4]);
    return $.datepicker.formatDate('yy-mm-dd', updated);
  }}
],

Now if you reload the page and execute a query the result should look similar to this page:

CMIS Search 4

Conclusion

In this post I returned to the jQuery DataTables to show how you can use the CMIS standard to populate a DataTable using a CMIS service request. I used Alfresco for this example, but this approach should also work for other systems that support CMIS (except for the joins to include the aspects). Once the CMIS browser binding is finalized, it will be even easier to develop CMIS clients using a framework like jQuery or YUI since it will replace the XML data format with JSON.

Read the previous tutorial

Read the first post jQuery DataTables and Alfresco. It covers jQuery DataTables with Alfresco’s OpenSearch keyword search as a back-end service. Read the third post in this series here. It covers jQuery DataTables and Spring web Scripts.

Update History

  • March 22, 2011: added formatting section and reference to previous tutorial
  • March 23, 2011: added note about aspects and some additional query examples
  • March 27, 2011: added a reference to the third tutorial in this series
jQuery DataTables and Alfresco

The jQuery library is a concise and easy to use Ajax library for rapid web development. It is also very usable to develop dashlets in Alfresco. When customizing Share the YUI library might be the prefered way to build dashlets, since it is the framework used to build Share, but compared to jQuery it is rather complex. Recently we used DataTables, a table plug-in for jQuery, to deliver a couple of Alfresco Explorer Dashlets. It is a an easy to use library with a lot of features. 

DataTables is designed and created by Allan Jardine and is dual licensed under the GPL v2 license or a BSD (3-point) license. Please make sure that you understand the licenses before you start using DataTables in your projects.

This tutorial assumes that you are familiar with Alfresco, JavaScript, HTML, JSON and preferably some XML. 

Download example code

The example code is available for download here.

Goals

The DataTables website contains a lot of examples to get you started. In this tutorial we will develop a table that enables the user to search for content stored in Alfresco using the keyword search web script that implements the OpenSearch standard. We will cover processing XML from the back-end, search, paging, showing and hiding columns and rendering column values to add custom markup. Finally we will add the ability to store the table’s state in a session cookie.

Initial setup

For this example I used a fresh install of Alfresco Enterprise 3.4.0, but you can also use the community version or an older Alfresco 3 release. The Alfresco service we will use for this example is the open search web script that is available out of the box. You can run it with a single keyword parameter and it will return an response based on the Atom feed protocol. You can give the keyword search a try using the following URL:

http://localhost:8080/alfresco/service/api/search/
  keyword.atom?q=document

This should return a response similar to this:

Open Search Feed

Take a look at the page source to see what the response data looks like. This will be helpful when we start processing the data in our table.

I created a ROOT folder under the webapps folder of my Tomcat distribution for my client-side page containing the table. You can also create an Alfresco Web Script.

Download the plug-in

The first step is to download the JavaScript library DataTables. Simply unpack the download under the ROOT folder. You will see a folder called media. It includes the required JavaScript and CSS files.

Create the file

The next step is to set up a basic HTML file for our client side table. We will start with a simple table listing the name of the documents returned by the OpenSearch Web Script. Create a file opensearch.html in the ROOT folder:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>jQuery DataTables and Alfresco OpenSearch Example</title>
  </head>
  <body id="dt_example">
    <div id="container">
      <h1>jQuery DataTables and Alfresco OpenSearch Example</h1>
      <div id="dynamic">
        <table class="display" id="example">
          <thead>
            <tr>
	      <th>Name</th>
            </tr>
	  </thead>
	  <tbody>
	    <tr>
	      <td class="dataTables_empty">Loading data from server</td>
	    </tr>
	  </tbody>
	</table>
      </div>
    </div>
  </body>
</html>

Add the libraries and stylesheets

The next step is to include the required JavaScript libraries and stylesheets. Add the following markup to the header of the page right under the title element:

<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">
<!-- here we will write our client-side JavaScript -->
</script>

Initialize the DataTable

The JavaScript code for the table is pretty straight forward. Setting up the table is extensively covered in the documentation. Add the following lines to the script tag:

function fnGetJSONData( sSource, aoData, fnCallback ) {
  /* this function will execute the keyword search */
}

$(document).ready(function() {
  $('#example').dataTable( {
    "bServerSide": true,
    "sAjaxSource": "/alfresco/service/api/search/keyword.atom",
    "bSort": false,
    "bPaginate": false,
    "fnServerData": fnGetJSONData
  } );
} );

Implement the callback function

The final step is to write the function that prepares the request to the Alfresco back-end and populates the JSON array we will pass to our table. DataTables uses a very simple approach to populate the table. You simply provide a JSON object containing the column values and a couple of metadata fields. A typical JSON response looks like this:

{"sEcho":1,
"iTotalRecords":"10",
"iTotalDisplayRecords":"10",
"aaData":[
  ["localizable.ftl"],
  ["translatable.ftl"],
  ["doc_info.ftl"],
  ["notify_user_email.ftl"],
  ["emailbody-textplain.ftl"],
  ["my_docs.ftl"],
  ["general_example.ftl"],
  ["example test script.js"],
  ["show_audit.ftl"],
  ["emailbody-texthtml.ftl"]
]}

This is however not the response that is returned by the keyword search. Alfresco’s keyword search uses the Atom feed protocol as defined in the OpenSearch data format.

Luckily the DataTables site contains an example plug-in created by Garry Boyce that shows how to process the OpenSearch data format. The following lines of code provide a first implementation for our call to the Alfresco back-end.

function fnGetJSONData( sSource, aoData, fnCallback ) {
	  
  $.ajax( {
    "dataType": 'xml', 
    "type": "GET", 
    "url": sSource, 
    "data": {"q":"document"}, 
    "success": function (data,textStatus,xmlHttpRequest) {
      var jData = $( data );			
      var json = {"sEcho": 1,"aaData" : []};
      json.iTotalRecords = 
        jData.find("[nodeName='opensearch:totalResults']").text();
      json.iTotalDisplayRecords = json.iTotalRecords;			
      var items = jData.find("entry").each(function(){
        json.aaData.push([
          $(this).find("title").text()
        ]);
      });
      fnCallback(json);
    }
  } );					
}

The most complex part is the function that prepares the JSON object used to populate the table. The data returned by the back-end is encoded in XML, so we have to build the JSON object using the XML response data. What we need is a JSON object like we showed in the previous paragraph. This function first creates an initial JSON object in a variable called json:

var json = {"sEcho": 1,"aaData" : []};

Next the iTotalRecords and iTotalDisplayRecords items are added to the JSON object.

json.iTotalRecords =  jData.find("[nodeName='opensearch:totalResults']").text();
json.iTotalDisplayRecords = json.iTotalRecords;

The final step is to iterate over the entry elements in the XML response to add the rows to aaData. For each entry we add the title element.

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("title").text()
  ]);
});

Now if you open the file opensearch.html in your browser you should see a page similar to this:

OpenSearch DataTable Example

Add search

This example is not very useful since it does not allow users to enter search terms. It will always request documents that contain the keyword ‘document’. To enable the user to enter search terms we need to extend our function.

The first step is to collect the request information. It will contain any search parameters the user enters in the text box. The request parameters are stored in aoData. It contains a key value pair for each parameter:

[
  {"name":"sEcho","value":1},
  {"name":"iColumns","value":1},
  {"name":"sColumns","value":""},
  {"name":"iDisplayStart","value":0},
  {"name":"iDisplayLength","value":-1},
  {"name":"sSearch","value":""},
  {"name":"bRegex","value":false},
  {"name":"sSearch_0","value":""},
  {"name":"bRegex_0","value":false},
  {"name":"bSearchable_0","value":true}
]

When the user enters a search term, the value is stored with the key sSearch. To make it easier to work with request parameters we can first put all the parameters in a map:

var params = {};
for ( var i=0 ; i <aoData.length ; i++ ) {
  var entry = aoData[i];
  params[entry.name] = entry.value;
}

We can then check if the user entered a search term:

if (params["sSearch"] == undefined || params["sSearch"] == "") {
  /* no search term entered */
} else {
  /* search term entered */
}

If there are no search terms entered we return an array with no rows:

var json = {"sEcho": undefined,"aaData" : []};
json.iTotalRecords = 0;
json.iTotalDisplayRecords = 0;
fnCallback(json);

Otherwise we add the search terms to the request parameters we send to the Alfresco backend, just like we did with the static value. I also added the dynamic value for the sEcho parameter:

 $.ajax( {
  "dataType": 'xml', 
  "type": "GET", 
  "url": sSource, 
  "data": {"q":params["sSearch"]}, 
  "success": function ( data,textStatus,xmlHttpRequest ) {
    var jData = $( data );
    var json = {"sEcho": params["sEcho"],"aaData" : []};
    json.iTotalRecords = 
      jData.find("[nodeName='opensearch:totalResults']").text();
    json.iTotalDisplayRecords = json.iTotalRecords;			
    var items = jData.find("entry").each(function(){
      json.aaData.push([
        $(this).find("title").text()
      ]);
    });
    fnCallback(json)
  }
} );

When you open the file opensearch.html in your browser you will see that there is initially no data. Once you start entering search values, you will see the results displayed in the table.

Adding paging

The Alfresco keyword search back-end also supports paging. To add paging you need to add two parameters to the search request: a parameter ‘c’ for the amount of rows you want to retrieve and a parameter ‘p’ for the page within the result set. So to retrieve the first five rows for documents that contain the word ‘document’ we can send the following request:

http://localhost:8080/alfresco/service/api/
  search/keyword.atom?q=document&c=5&p=1

The DataTables implementation behaves slightly different when it comes to paging. In stead of providing a parameter for the page it will provide the count for the first row we want to display out of the total records. So if we want to display the next five rows, the pager provides us with a key iDisplayStart with value 6 and not a value 2 for the second page. We can fix this issue using the following calculation:

var start = simpleMap["iDisplayStart"]/simpleMap["iDisplayLength"] + 1;

Our updated Ajax request will now look like this:

var start = (params["iDisplayStart"]/params["iDisplayLength"]) + 1;
var length = params["iDisplayLength"];
var query = params["sSearch"];

$.ajax( {
  "dataType": 'xml', 
  "type": "GET", 
  "url": sSource, 
  "data": {"q":query,"c":length,"p":start},
  "success": function ( data,textStatus,xmlHttpRequest ) {
    var jData = $( data );
    var json = {"sEcho": params["sEcho"],"aaData" : []};
    json.iTotalRecords = 
      jData.find("[nodeName='opensearch:totalResults']").text();
    json.iTotalDisplayRecords = json.iTotalRecords;			
    var items = jData.find("entry").each(function(){
      json.aaData.push([
        $(this).find("title").text()
      ]);
    });
    fnCallback(json)
  }
} );

To show the paging in our table we need to update some initialization parameters:

$(document).ready(function() {
  $('#example').dataTable( {
    "bServerSide": true,
    "sAjaxSource": "/alfresco/service/api/search/keyword.atom",
    "bSort": false,
    "bPaginate": true,
    "sPaginationType": "full_numbers",
    "fnServerData": fnGetJSONData
  } );
} );

When you load the page in your browser you should now be able to page through the results.

DataTable

Setting a delay to reduce server load

What you will see when you use a tool like Firebug is that a request is submitted to the Alfresco server for every character we enter in the search field. To prevent this, we can add a delay. Developers Zygimantas Berziunas and Allan Jardine created a plug-in function called fnSetFilteringDelay() that provides this functionality. You can find the code on the DataTables website. I added the function as a separate JavaScript file in the media folder with the other JavaScript files and imported the file in opensearch.html:

<script type="text/javascript" src="media/js/jquery.setFilteringDelay.js"></script>

Once this is done, you can add the function to the table initialization code:

$(document).ready(function() {
  $('#example').dataTable( {
    "bServerSide": true,
    "sAjaxSource": "/alfresco/service/api/search/keyword.atom",
    "bSort": false,
    "bPaginate": true,
    "sPaginationType": "full_numbers",
    "fnServerData": fnGetJSONData
  } ).fnSetFilteringDelay(400);
} );

When you open your browser and use Firebug, you will see that the request to the back-end is not submitted for each character entered in the search field.

Adding columns

Now that we provided search and paging, we can add columns to our table to provide more information about the content we retrieve. To add the author for example, we need to add the value for the author to the JSON array:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("title").text(),
    $(this).find("author").find("name").text()
  ]);
});

And we need to add a header to our table:

<thead>
  <tr>
    <th width="80%">Title</th>
    <th width="20%">Author</th>
  </tr>
</thead>

That is all we need to do in order to add a column.

Rendering columns

To complete our table we will add the icon for the type of document and a link to the document. Again the first step is to add the columns to our JSON array:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("icon").text(),
    $(this).find("link").attr("href"),
    $(this).find("title").text(),
    $(this).find("author").find("name").text()
  ]);
});

To add the icon and the link to the name column we need to hide the columns with the icon reference and the link and we need to add a renderer that adds the markup for the icon image and the link. To do this add a parameter aoColumnDefs to the table initialization code:

"sPaginationType": "full_numbers",
"aoColumnDefs": [ 
{
  "fnRender": function ( oObj ) {
    return '<img src="' + oObj.aData[0] + 
      '" /> <a href="' + oObj.aData[1] + 
      '">' + oObj.aData[2] + '';
  },
  "aTargets": [ 2 ]
  },
  { "bVisible": false,  "aTargets": [ 0,1 ] 
}],
"fnServerData": fnGetJSONData

Even when you hide the columns, you need to add them to the table:

<thead>
  <tr>
    <th width="0%">Icon</th>
    <th width="0%">Link</th>
    <th width="80%">Title</th>
    <th width="20%">Author</th>
  </tr>
</thead>

When you open the file in your browser you should see a page similar to this:

OpenSearch DataTables

When you click on the title, the browser will show a preview of the file from the Alfresco backend.

Saving the state in a session cookie

Now when you return to the table, you have to resubmit your search and navigate to the page that you left when you clicked the link. To solve this we simply add the following parameter to the table initialization code:

"bStateSave": true,
"fnServerData": fnGetJSONData

This will save the state of the table in a session cookie.

Adding a style class to a table cell

This final example shows how you can add a style class to a specific table cell. We will add a new column displaying the score and if the score is greater than 0.5 we will display the score in red and otherwise we will display the score in gray.

The first step is to add the score to the JSON object used to populate the table:

var items = jData.find("entry").each(function(){
  json.aaData.push([
    $(this).find("icon").text(),
    $(this).find("link").attr("href"),
    $(this).find("title").text(),
    $(this).find("author").find("name").text(),
    $(this).find("[nodeName='relevance:score']").text()
  ]);
});

The next step is to add the column to the table:

<thead>
  <tr>
    <th width="0%">Icon</th>
    <th width="0%">Link</th>
    <th width="70%">Title</th>
    <th width="20%">Author</th>
    <th width="10%">Score</th>
  </tr>
</thead>

We will then add the classes to the stylesheet demo_table.css located in the media/css folder:

td.high {
    color: #FF0000;
}

td.low {
    color: #BEBEBE;
}

The final step is to add a new parameter called fnRowCallback to our table initialization code:

"fnRowCallback": function( nRow, aData, iDisplayIndex ) {
  if (parseFloat(aData[4].replace(',','.')) > Number(0.5)) {
    $('td:eq(2)', nRow).addClass('high');
  } else {
    $('td:eq(2)', nRow).addClass('low');
  }
  return nRow;
},

Note: Since I am using a Dutch system locale I had to replace the comma in the decimal numbers with a dot using a replace (aData[4].replace(',','.')).

The result will look similar to this:

Example 5

Conclusion

The jQuery DataTables plug-in provides powerful features to create tables that retrieve data from an Alfresco back-end. We used a standard out of the box Web Script to retrieve our data, but you can of course create your own back-end Web Scripts. You can also use CMIS to retrieve back-end data. You can use this approach to create Web Script pages, Alfresco dashlets or to add tables to your custom Spring Surf client.

We covered quite some features of the jQuery DataTables plug-in, but there is a lot more you can do. You can for example add sorting, style your table using ThemeRoller, add row and column highlighting, add additional search fields or submit data back to the server.

Read the follow up  tutorial

The follow up post jQuery DataTables, CMIS and Alfresco provides a similar approach to populate DataTables using the CMIS standard.