On almost every topic
Processing CMIS with Freemarker in Alfresco

Since its release in version 2.1 Alfresco Web Scripts proved to be extremely useful as a framework to build data oriented services and UI Components. They are even used to develop complete application integrations. Thanks to Web Scripts Alfresco was for example able to deliver one of the first CMIS implementations.

Officially Web Scrips are now part of the Spring Surf Framework, but the Spring community never embraced Surf and it will not surprise me if the maintenance and development of the framework returns to Alfresco in the near future. I think the limited tool support also contributed to the lack of success.

I have always been a great fan of Web Scripts. It is a simple yet powerful MVC framework that allows you to build services with just a couple of simple files. You implement the controller using Server Side JavaScript and the view using Freemarker, a very powerful templating language that can output any text oriented data wheter it is HTML, JSON or some XML based format.

Remote Client

In Alfresco Explorer you have direct access to model objects, but with Alfresco Share this is not the case. Alfresco Share is a remote client that requires you to collect data from the Alfresco backend using a service layer. You can write your own backend services with Web Scripts, but you can also use CMIS or one of the other backend services provided out of the box.

The problem with CMIS is that each request returns an XML response and XML and JavaScript are not a great marriage. With the upcoming CMIS 1.1 you can use JSON as an alternative binding. To some extend you can use the Abdera JavaScript client, but Abdera focuses on Atom entries and as far as I know there is no extension that allows easy access to all the CMIS specific properties.

After writing a lot of lines of code in JavaScript (click here for an example from Alfresco’s 3.4 documentation), I got tired of coding and decided to use a more simple approach by consuming the CMIS data using Freemarker. I am aware that it is the responsibility of the controller to deliver an easy to use model to the view, but Freemarker provides proper support for XML including XPath support.

Example Web Script

The following example Web Script shows how you can pass the CMIS response to the view in the controller layer and use the data within the Freemarker template.

The first step is to define a new Web Script in Alfresco Share. Navigate to the ‘tomcat/shared/classes/alfresco/web-extension/site-webscripts’ folder and create the Web Script. A Web Script consists of a Web Script descriptor, an optional JavaScript controller and a Freemarker template for the view. First create a folder ‘samples’ for the example Web Scripts. In this folder create a file called ‘sample-cmis.get.desc.xml’ with the following contents:

<webscript>
<shortname>XML example</shortname>
<description>XML Example</description>
<url>/sample/cmis</url>
</webscript>

The next step is the controller. In the same folder create a file ‘sample-cmis.get.js’ with the following contents:

var connector = remote.connect("alfresco");
var result = connector.get("/cmis/p/sample.txt");
model.doc = stringUtils.parseXMLNodeModel(result.getText());

This script first creates a connection with the Alfresco backend. The connection is then used to execute a CMIS request. The request ‘/cmis/p/sample.txt’ retrieves the object data by path for a file called ‘sample.txt’ located in the Company Home root space. I simply created a plain text file in Alfresco with some dummy content and name, title, author and description properties. The CMIS response is then passed to the Freemarker template as an XML document using the parseXMLNodeModel method available in the root scoped object stringUtils (see here).

In order to retrieve values from the model in Freemarker you can now use the XML document. Create a file called ‘sample-cmis.get.ftl’ and add the following lines:

<#ftl ns_prefixes={ "D":"http://www.w3.org/2005/Atom" }>
<p>${doc.entry.title}</p>

Since CMIS uses namespaces to combine several schema’s, you need to register the prefixes of the namespaces. In the example above only the default ‘atom’ namespace is registered. Freemarker uses a D as a prefix for the default namespace. If you do not register the namespaces, including the default namespace, Freemarker will not retrieve any values and throw an exception.

In order to register the Web Script in Share you can visit http://localhost:8080/share/service/index.html and click the refresh button. If you then visit the page http://localhost:8080/share/page/sample/cmis you should see a page with the name of the document.

Tip: if you set the mode of Share to ‘development’ in ‘surf.xml’ you do not need to refresh the Web Scripts every time you make changes. You can find the file in ‘tomcat/webapps/share/WEB-INF’.

Adding namespaces

In order to, for example, print the icon of the document, we need to add the Alfresco namespace. Freemarker requires the use of square brackets when selecting elements with prefixes because otherwise the colon would confuse the Freemarker engine. The following template outputs the icon and name of the document:

<#ftl ns_prefixes={ "D":"http://www.w3.org/2005/Atom",
"alf":"http://www.alfresco.org" }>
<p><img src="${doc.entry["alf:icon"]}" />${doc.entry.title}</p>

When you revisit the page you should see a page similar to this:

Using XPath

You can also use XPath to select values from the CMIS document. The following template retrieves the Alfresco author property using an XPath expression:

<#ftl ns_prefixes={ "D":"http://www.w3.org/2005/Atom",
"alf":"http://www.alfresco.org",
"cmis":"http://docs.oasis-open.org/ns/cmis/core/200908/" }>

<p><img src="${doc.entry["alf:icon"]}" />${doc.entry.title}</p>
<p>Author: ${doc["//cmis:propertyString[@propertyDefinitionId = 'cm:author']/cmis:value"] </p>

You can parse a date value in order to reformat the date like this:

<p>Updated: ${doc.entry.updated?date("yyyy-MM-dd'T'HH:mm:ss")?string("yyyy-MM-dd HH:mm:ss")}</p>

In order to retrieve the value, a third namespace prefix was registered to select elements from the ‘cmis’ namespace.

Using The List Directive

You can also retrieve multiple propeties using XPath. The following template lists all the CMIS property display labels and values:

<#ftl ns_prefixes={ "D":"http://www.w3.org/2005/Atom",
"alf":"http://www.alfresco.org",
"cmis":"http://docs.oasis-open.org/ns/cmis/core/200908/" }>
<p><img src="${doc.entry["alf:icon"]}" />${doc.entry.title}</p>
 <ol>
<#list doc["//cmis:properties/*/cmis:value"] as p>
<li>${p?parent.@displayName}: ${p}</li>
</#list>
</ol>

In order to include the properties of aspects, you can for example use the XPath local-name() function:

<#list doc["//*[local-name() = 'properties']/*/cmis:value"] as p>
<li>${p?parent.@displayName}: ${p}</li>
</#list>

When you revisit the page, the result should look simliar to this:

Building a model

To make things easier to process in your template you can write a function that returns a map of name and propety values. You can then use the map as you are used to in backend Web Scripts. The following function retrieves the properties and adds them to a map using the property name as a key:

<#function properties doc>
<#assign s = "{"> <#list doc["//*[local-name() = 'properties']/*/cmis:value"] as p>
<#assign s = s + "\"${p?parent.@propertyDefinitionId}\":\"${p}\"">
<#if p_has_next> <#assign s = s + ","> </#if> </#list>
<#assign s = s + "}">
<#return s?eval>
</#function>

This is just a basic function that works fine for non-repeatable fields. You can execute the function and use the properties for example in a list directive:

<#assign props = properties(doc)>
<ol>
<#list props?keys as key>
<li>${key}: ${props[key]}</li>
</#list>
</ol>

You can of course also get individual properties by property name. The following line outputs the description of the document:

<p>Description: ${props["cm:description"]}</p>

The following listing shows the complete Freemarker template using the function:

<#ftl ns_prefixes={ "D":"http://www.w3.org/2005/Atom",
"alf":"http://www.alfresco.org",
"cmis":"http://docs.oasis-open.org/ns/cmis/core/200908/" }>
<#function properties doc>
<#assign s = "{">
<#list doc["//*[local-name() = 'properties']/*/cmis:value"] as p>
<#assign s = s + "\"${p?parent.@propertyDefinitionId}\":\"${p}\"">
<#if p_has_next>
<#assign s = s + ",">
</#if>
</#list>
<#assign s = s + "}">
<#return s?eval>
</#function>
<p><img src="${doc.entry["alf:icon"]}" /> ${doc.entry.title}</p>

<#assign props = properties(doc)>
<ol>
<#list props?keys as key>
<li>${key}: ${props[key]}</li>
</#list>
</ol>

When you visit the example page again you will see a listing of all the property names and values:

Conclusion

Although from a design point of view this approach might not be perfect, it provides a means to consume CMIS responses in just a couple of lines of code. If you have other suggestions to consume CMIS documents in Web Scripts, please let me know.

The Learning by Example page from the Freemarker Manual can be a useful resource when processing XML documents with Freemarker.

Updates

  • Made some minor updates on January, 26

References

Oracle has a very direct approach to persuade customers to move from Documentum to Oracle. I&#8217;ve never been that interested in Oracle&#8217;s content management offerings. I assume it is the former Stellent product and I have no idea to what extend they invest in the further development of this product. But I am glad I moved away from Documentum in 2006 to focus on Alfresco&#8217;s open source content management, especially if you look into the new features the upcoming release is going to offer. 

Oracle has a very direct approach to persuade customers to move from Documentum to Oracle. I’ve never been that interested in Oracle’s content management offerings. I assume it is the former Stellent product and I have no idea to what extend they invest in the further development of this product. But I am glad I moved away from Documentum in 2006 to focus on Alfresco’s open source content management, especially if you look into the new features the upcoming release is going to offer. 

This post by Alfresco’s Paul Hampton provides a good overview of new features in the Alfresco 4 Community version. If I had to list some of the new features it would be the Activity workflow engine, the new Apache Solr based index server, the new details page and drag and drop.

The new ‘social’ features like social channel publishing and the ability to follow influential users are also great, but the other features will help us to also deliver better solutions for the more conventional content management use cases.

Alfresco’s CMO Todd Barr about Alfresco Team, a low-cost Alfresco subscription that allows teams, departments and small organizations to get a supported Alfresco distribution for content collaboration. To some extent Alfresco Team is a competitor to Alfresco Community that is currently used by many small organizations. The product seems similar to Alfresco Share, but there are a couple of new features, like support for mobile devices, that will be part of Community and Enterprise later this year. Alfresco Team is fully compatible with Alfresco Enterprise allowing customers to upgrade.

Drupal LogoAt the NLDITA 2011 conference Kristof Van Tomme held an interesting talk about DITA support in Drupal. Although the DITA integration module is not yet finished, thanks to the modules available in Drupal, there is already quite some functionality available. Editing DITA documents is possible using forms created with the Content Construction Kit (CCK) and you can create DITA maps using the taxonomy support and Graphmind, Drupal’s mindmap module. There is also basic support for the DITA toolkit to create output in different formats. What I like about the project is that they focus on usability to provide a very user friendly module to author DITA topics and maps. They are also working on providing a community platform to enable end users of the generated documentation to provide direct feedback.

There is currently no XML editor integrated since the current open source offerings are not considered user friendly. I guess writing a proper editor for DITA is not an easy task. Even most commercial offerings can only be used by technical writers who are used to deal with structured content.

A new blog launched by Alfresco ‘where content is the conversation’. The blog seems to focus on more business oriented posts about content management. The blog also invites others to contribute.

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.

Alfresco kicked off their fiscal year with a meeting last week in Orlando. About 100 Alfresco employees and 50 partners attended two days of Alfresco-led talks on business and technical topics.

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.