On almost every topic
Using CMIS in Alfresco 4.0 Web Scripts

Through an email Jeff Potts commented on my recent post Processing CMIS with Freemarker. In his email Jeff suggested to have a look at the ‘cmis’ root object that is available in Alfresco Community 4.0. I assume it will also be available in the upcoming Alfresco Enterprise 4.0.

Note: Jeff warned that the cmis root object appears to be broken in the current Community release 4c, but it might be fixed in the upcoming release 4d.

Basic Example

To test it in my Alfresco Community 4b release I created a new Web Script in Alfresco Share similar to the one I used to test the approach described in the Freemarker related tutorial. It retrieves a file by path and displays the properties. 

The first step is to create the Web Script descriptor. Create a file called ‘sample-opencmis.get.desc.xml’ with the following contents:

<webscript>
<shortname>Open CMIS example</shortname>
<description>Open CMIS Example</description>
<url>/sample/opencmis</url>
</webscript>

The next step is to create the controller. We start with a simple example to test the root object. It simply retrieves the root folder and adds it to the model. Create a file called ‘sample-opencmis.get.js’ with the following contents:

var cmisConnection = cmis.getConnection();
var cmisSession = cmisConnection.getSession();

folder = cmisSession.getRootFolder();

model.folder = folder;

Finally create the view by adding a file called ‘sample-opencmis.get.ftl’ with the following contents:

<h2>${folder.name}</h2>

Now when you refresh your Web Scripts at http://localhost:8080/share/service/index.html you should see a new Web Script called ‘sample/opencmis’. When you run this script using the URL http://localhost:8080/share/page/sample/opencmis, you should see a page similar to this:

Retrieving a Document

You can also use the cmis root object to retrieve content by path or to execute queries. For example to get a file ‘sample.txt’ located in the Company Home folder, you can add the following lines to the JavaScript controller:

doc = cmisSession.getObjectByPath("/sample.txt");
model.doc = doc;

You can then use the document in your view, for example to print the name and list the properties:

<h2>${doc.name}</h2>
<ul>
<#list doc.properties as p>
<li>${p.definition.displayName}: ${p.valuesAsString}</li>
</#list>
</ul>

This will output the following information:

You can for example also access a number of properties directly like the creationDate, createdBy or versionLabel. To access a property by queryName or id, you can use the following method:

${doc.getPropertyValue("cmis:objectTypeId")}

Next Steps

Within the next couple of days I hope to find some time to further explore the features and limitations of the cmis root object, for example how to execute queries, how to set up a remote connection, or even how to create or update content, folders, properties and aspects.

References

The cmis root object does not seem to be extensively documented in the alfresco documentation. Exploring the documentation of the Alfresco OpenCMIS Extension can be very helpful to lookup the available objects and methods. You can adjust them to meet the conventions of either the JavaScript controller or the Freemarker view.

Updates

  • February 2, 2012: added accessing properties by name.

References

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

Using Alfresco Composite Rendition to Render PDF

This is a follow-up to the post Add an XML to PDF Transformer to Alfresco. In this post I looked into the options to add and use an XSL-FO processor using configuration files and some JavaScript. The post ended with the remark that Composite Renditions are not exposed in the JavaScript API. This post provides an example of a custom action that creates a PDF rendition based on XSL-FO.

Note: in order to make these examples work, you need to follow the configuration steps in the previous post. This post assumes you are familiar with customizing Alfresco using Java.

Composite Rendition

A Composite Rendition allows you to create a rendition from one mimetype to another using one or more intermediate transformations. With XSL-FO for example you would normally first transform the XML document into XSL-FO and then transform the result to the desired output format, for example PDF.

Custom Action

To create PDF renditions for XML files using XSL-FO you can write a custom action in Java that creates a Composite Rendition to first transform the XML document into an XSL-FO document and then transforms the result into a PDF. The PDF is then stored as a rendition associated to the XML document.

The following Java class demonstrates this approach. It is a custom action that takes a single parameter to pass the stylesheet:

package com.someco.action;

import java.util.List;

import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.rendition.executer.ReformatRenderingEngine;
import org.alfresco.repo.rendition.executer.XSLTRenderingEngine;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.rendition.CompositeRenditionDefinition;
import org.alfresco.service.cmr.rendition.RenditionDefinition;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;

public class FoRenditionActionExecuter extends ActionExecuterAbstractBase {

  public static final String NAME = "fo";

  public static final String PARAM_TEMPLATE_REF = "template";

  RenditionService renditionService;

  @Override
  protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {

    RenditionDefinition xslDefinition = renditionService
      .createRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "xslRenderingDefinition"), XSLTRenderingEngine.NAME);

    NodeRef template = (NodeRef) action
      .getParameterValue(PARAM_TEMPLATE_REF);

    xslDefinition.setParameterValue(
      XSLTRenderingEngine.PARAM_TEMPLATE_NODE, template);
    xslDefinition.setParameterValue(
      ReformatRenderingEngine.PARAM_MIME_TYPE, "text/xsl");

    RenditionDefinition pdfDefinition = renditionService
      .createRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "pdfRenderingDefinition"), ReformatRenderingEngine.NAME);
		
    pdfDefinition.setParameterValue(
      ReformatRenderingEngine.PARAM_MIME_TYPE,
      MimetypeMap.MIMETYPE_PDF);

    CompositeRenditionDefinition compositeDefinition = renditionService
      .createCompositeRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "compRenderingDefinition"));

    compositeDefinition.addAction(xslDefinition);
    compositeDefinition.addAction(pdfDefinition);

    renditionService.render(actionedUponNodeRef, compositeDefinition);
  }

  @Override
  protected void addParameterDefinitions(List paramList) {
    paramList.add(new ParameterDefinitionImpl(PARAM_TEMPLATE_REF,
      DataTypeDefinition.NODE_REF, true,
      getParamDisplayLabel(PARAM_TEMPLATE_REF)));
  }

  public RenditionService getRenditionService() {
		return renditionService;
  }

  public void setRenditionService(RenditionService renditionService) {
		this.renditionService = renditionService;
  }

}

In order to make the custom action available in Alfresco we need to add it as a Spring Bean:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
  
<beans>
 
  <bean id="fo" class="com.someco.action.FoRenditionActionExecuter"
    parent="action-executer">
    <property name="publicAction">
      <value>false</value>
    </property>
    <property name="renditionService">
      <ref bean="RenditionService" />
    </property>
  </bean>

</beans>

Execute the action

Next restart Alfresco. You can test the custom action by writing a small JavaScript that executes the action by providing an XML document and an XSL stylesheet that creates XSL-FO output. The FOP distribution provides some examples that you can use. The following code demonstrates how to execute the action using JavaScript:

var fo = actions.create("fo");
fo.parameters.template = companyhome.childByNamePath("/XML/projectteam2fo.xsl");
fo.execute(document);

This code creates a PDF rendition for the given document (in this case a file called projectteam.xml, an example provided by FOP). The rendition is stored as a child of the source document.

Custom Transformer Java Implementation

Once you have started writing Java code, you might as well want to replace the transformer configuration with a Java implementation. The Apache FOP Java library is already on Alfresco’s class path, so a Java implementation does not require a separate install of Apache FOP.

The following class is a basic implementation to provide the custom transformer in Java to replace the transformer defined in the file custom-transformer-context.xml:

package com.someco.transform;

import java.io.BufferedOutputStream;
import java.io.OutputStream;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;

import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformer2;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.TransformationOptions;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;

public class FoContentTransformer extends AbstractContentTransformer2 {

  @Override
  public boolean isTransformable(String sourceMimetype,
      String targetMimetype, TransformationOptions options) {
    if (sourceMimetype.equalsIgnoreCase(MimeConstants.MIME_XSL_FO)
        && targetMimetype.equalsIgnoreCase(MimetypeMap.MIMETYPE_PDF))
      return true;
    return false;
  }

  @Override
  protected void transformInternal(ContentReader reader,
      ContentWriter writer, TransformationOptions options)
      throws Exception {

    FopFactory fopFactory = FopFactory.newInstance();

    OutputStream out = new BufferedOutputStream(
        writer.getContentOutputStream());

    try {
      Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, out);

      TransformerFactory factory = TransformerFactory.newInstance();
      Transformer transformer = factory.newTransformer();

      Source src = new StreamSource(reader.getContentInputStream());

      Result res = new SAXResult(fop.getDefaultHandler());

      transformer.transform(src, res);

    } finally {
      out.close();
    }
  }

}

Next update the file custom-transformer-context.xml. Replace the current transformer with a Spring Brean that loads the Java implementation:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>

  <bean id="transformer.FOP.PDF"
    class="com.someco.transform.FoContentTransformer"
    parent="baseContentTransformer" />

</beans>

Restart Alfresco and try to run the JavaScript code to create the rendition to PDF.

Read the follow-up post Persisting Alfresco Renditions to learn how to persist renditions to allow Alfresco to update the rendition on any properties update. 

Last updated: October 14, 2011

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.

Spring Surf with an Alfresco backend

Spring Surf is actively under development. Recently authentication against Alfresco was added to the Surf Quick Start application making it very simple to add an Alfresco backend to your Surf application. To get an idea in what direction this is heading to, I added a page to the example application that displays blog posts from Alfresco Share. The page requires authentication.

Note: this tutorial demonstrates Spring Surf features that are not stable yet.

Environment

I have an Alfresco Enterprise 3.2r backend running on http://localhost:8080/alfresco and will run Jetty on my Surf project. I guess it will also work with Alfresco Community. Make sure that you have Maven installed.

Download Spring Roo

The first step is to download and install Spring Roo from the Spring Source Community download page at http://www.springsource.com/download/community. Spring Roo is a rapid application development tool for Java developers focusing on productivity and ease of use.

The next step is to download the latest snapshot for the Spring Surf Roo Addon from http://www.springsurf.org/downloads.html. Of course you can also create a new build. Check the Spring Source Development Guide for instructions on how to do this. In order to build from source you need a Subversion client and Maven. Add the snapshot for Roo to the dist directory of your Roo installation (on my Windows machine it is C:\Apps\spring-roo-1.0.2.RELEASE\dist). It might be useful to open the Surf Roo Command Index as a reference.

Create a project

We are now ready to create a new project with Roo. From the command line navigate to a directory where you want to create your application, create a new directory for your Surf application and start the Roo shell:

c:\projects>mkdir surf-alfresco

c:\projects>cd surf-alfresco

c:\projects\surf-alfresco>roo

Your Roo shell looks similar to this:

We are now ready to start coding. First create a new project:

roo>project --topLevelPackage com.example.surf

Tip: roo supports completion using the TAB key, so if you type project and then TAB roo will suggest to add the topLevelPackage argument.

Now install Surf:

roo>surf install

When you install Surf, Roo creates an example application to get you started. We will add authentication to this application and a new page showing blog posts from an Alfresco Share site called Company.

Enable authentication

In order to to enable authentication against the Alfresco repository, open the file surf.xml in the src/main/webapp/WEB-INF directory and comment out the following line:

<user-factory>webframework.factory.user.alfresco32</user-factory>

In addition I made some changes to the file login.ftl in the src/main/webapp/WEB-INF/templates/sample directory. I changed these lines:

<input name="success" type="hidden" value="/"/>
<input name="failure" type="hidden" value="/type/login"/>

To:

<input name="success" type="hidden" 
  value="${url.context}/sample/userinfo"/>
<input name="failure" type="hidden" 
  value="${url.context}/sample/login"/>

This will show the user information on a successful login and the login page when the login is not successful. The templates for login, logout and the user information are already added as example templates.

Test the application

You can now test the application. To do this exit roo and run Jetty:

roo>exit
c:\apps\surf-allfresco>mvn jetty:run

Make sure Alfresco is also running and visit the Surf application by visiting http://localhost:8180/

You should see the following page. This is the example application we created by installing Surf:

Now visit the page http://localhost:8180/sample/login. You should see a page like this:

Type an Alfresco user name and password en click Log In. The system should now display the login details.

You are now able to authenticate, so the next step is to add a page that requires the user to login. To stop Jetty you can simple type Ctrl-C.

Note: you are not required to stop Jetty, you can also open a new command shell. Jetty will notice changes and do a reload.

Create a new component

We start with creating a new Surf component. Surf components basically are Web Scripts. If you are not familiar with Web Scripts, you might want to read some documentation first. You can find it here.

First we create a folder called posts under the WEB-INF/webscripts directory. We then create a new file called posts.get.desc.xml with the following content:

<webscript>
  <shortname>Company Posts</shortname>
  <description>Company Posts</description>
  <url>/news/posts</url>
  <authentication>user</authentication>
</webscript>

This is the descriptor for our new component. It provides a name and description and a URL to provide access to the resource. We also set the authentication to user, since we require the user to be authenticated when he/she requests blog posts. By default authentication is set to none.

The next step is to create the controller. The controller knows what to do when the user requests the resource (in this case showing the blog posts stored in Alfresco). The controller is implemented using JavaScript. Create a file called posts.get.js with the following contents:

var connector = remote.connect("alfresco");
var result = connector.get("/api/blog/site/company/blog/posts");
logger.log(result);
var posts = eval('(' + result + ')');
model.posts = posts;

The connector’s get method retrieves the blog posts from a site called company. You can change company with the name of any Alfresco Share site. If you do not have a site yet, you can create one and add some blog posts to it.

The final part of the component is the view. The Web Scripts Framework uses Freemarker as the default template engine. Create a file called posts.get.html.ftl and add the following contents:

<div>
<#if posts.items?exists>
  <#list posts.items as post>
    <h2>${post.title}</h2>
    ${post.content}
  </#list>
<#else>
There are no posts.
</#if>
</div>

I must admit that this template is not going to return a stunning web page, but for now it will work just fine.

One final change I made is to forward the user to the news page on a succesful login in stead of the user information by editing the login.ftl once more:

<input name="success" type="hidden" value="${url.context}/news" />

Create a new page

We now have to revisit Roo to create a new page, add the required components to the page including our blog posts component and to add the page to the site navigation. To do this start Roo and first create the page:

roo> surf page create --id news --template home

We create a new page based on the existing template for the Home Page. Next we add the blog posts to the main region of the page:

roo> surf component create --page news --region main
  --url /news/posts

We can then run a report to see if we need to add addditional components:

roo> surf report page --id news
-----------------------------------------------------------
Report on Page news

-----------------------------------------------------------
Basic Information

Id: news
Name: news
Path: pages\news\news.xml
Instance: home
Template: home
-----------------------------------------------------------
Page Scoped Components

Region: main Url: /news/posts
Region: side Scope: page Configued: false

-----------------------------------------------------------
Template Scoped Components

Region: header Url: /company/header
Region: horznav Url: /navigation/horizontal
Region: footer Url: /company/footer

-----------------------------------------------------------
Global Scoped Components

-----------------------------------------------------------
Page Associations

-----------------------------------------------------------

It seems that the component for the side region is missing, so we simply add the same component as the one used for the home page. I simply looked it up in the file pages/home/home.xml and it is the component /home/side:

roo> surf component create --page news --region side 
  --url /home/side

The last thing we need to do is to add our new page to the horizontal navigation by creating an association between the page home and our new page called news:

code>roo> surf page association create --sourceId home 
  --destId news

We can now exit Roo:

roo>exit

Before starting Jetty again, visit the page news.xml and set authentication to user instead of none. Save the file and run Jetty again.

c:\projects\surf-alfresco>mvn jetty:run

Test the new page

Now test the application by visiting http://localhost:8180 again. You should see a news button added to the horizontal navigation:

When you click the news page, the system shows the login page again and returns to the news page on a successful login:

We now have added a page to Surf’s Quick Start application that enables the user to authenticate against Alfresco and read blog posts stored in the backend repository.

Conclusion

Spring Surf and Spring Roo provide a very promising development approach enabling the Java developer to focus on getting things done without having to create huge amounts of code and a lot of configuration files. The built-in support for Alfresco makes Surf a perfect framework to build your Alfresco front-end application. With the upcoming CMIS (Content Management Interoperability Services) standard we will even have a standard interface to access content repositories.