On almost every topic

In Alfresco 4.0 you can transfer files from the repository to a remote file system. You can use it for example to publish static content to a web server. In older Alfresco versions this was only possible for Web Projects that used Alfresco’s AVM (Advanced Versioning Manager) repository.

I had some difficulties setting up the target. This feature does not seem to be documented very well. Luckily Michael McCarthey of Tribloom documented the basic steps on his blog.

Alfresco supports replication jobs that allow you to schedule transfers of content and structure between a source and a target system. In order to use it you first configure a Transfer Target, being a remote Alfresco instance or a File System Transfer Receiver, and then you use these targets in your replication jobs.

In order to succesfully transfer files to a remote file system you need to change the type of the transfer folder to File Transfer Target and you have to make sure to set the end point to /alfresco-ftr/service/api/transfer

This Alfresco forum post might also be helpful. It explains the difference between the payload property in the replication job and the root folder property in the transfer target.

Today Jeff Potts, Alfresco’s Chief Community Officer, writes on Social Content that the Alfresco AVM (Alternative Versioning Model) will no longer be supported from Alfresco 5. The AVM was an alternative repository implementation in Alfresco to support Web Content Management use cases. It was designed as some sort of version control system with support for repository virtualization. It was originally designed by Kevin Cochrane who left Alfresco in 2008 to work for Day Software

I am must say that I am very happy with Alfresco’s decision to get rid of the AVM store. The whole idea behind the virtualization was interesting, but having to work with two repositories always caused a lot of problems. The solution was also too complex for most end users. At Incentro we support a couple of publishers who use Alfresco and publishing to the internet is only one of their channels. For these customers we always had to write custom actions to copy content from one repository to the other using the CrossRepositoryCopyService. End users, including non-technical authors, had to execute a copy action and then they where forced to navigate to the Web Project in order to deploy the content to a web server.

Starting from Alfresco 4.0 you can deploy directly from the DM (Document Management) repository to a web server. I haven’t tried it yet, but you might even be able to add a publishing channel to do the job.

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

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. 

Alfresco Rendition Services

I recently posted three tutorials about transforming XML to PDF using XSL-FO and Alfresco’s Rendition Service. The tutorials do not cover every detail, but they might be helpful to those who are interested in using renditions in Alfresco. Alfresco’s Rendition Service allows you to automatically render content in alternative forms when the content or properties are updated.

  1. Add an XML to PDF Transformer to Alfresco
  2. Using Alfresco Composite Rendition to Render PDF
  3. Persisting Alfresco Renditions
Persisting Alfresco Renditions

This post is a follow-up to the post Using Alfresco Composite Rendition to Render PDF. In this tutorial we will update the Java code in order to persist the composite rendition that creates a PDF rendition using Apache FOP and XSL-FO as an intermediate format.

Follow Alfresco

When you persist a rendition in Alfresco, the system will update a rendition every time the document properties are updated. You can follow Alfresco’s rendition behaviour by adding the following line to the log4j.properties file:

log4j.logger.org.alfresco.repo.rendition=debug

Persist a rendition

To make a rendition persistent you need to store the rendition definition using the folowing method:

renditionService.saveRenditionDefinition(compositeDefinition);

In addition to adding this line, I also decided to check if the rendition definition already exists, since you call this code every time you add a new rendition to a document.

Updated code

In the following class the updates are highlighted:

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;

  public RenditionService getRenditionService() {
    return renditionService;
  }

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

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

    QName compRenderingName = QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "compRenderingDefinition");

    CompositeRenditionDefinition compositeDefinition = (CompositeRenditionDefinition) renditionService
        .loadRenditionDefinition(compRenderingName);

    if (compositeDefinition == null) {

      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);

      compositeDefinition = renditionService
          .createCompositeRenditionDefinition(compRenderingName);

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

      renditionService.saveRenditionDefinition(compositeDefinition);
    }
    
    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)));
  }

}

Test the rendition

Now if you create a rendition using this custom action, the system will first check if there is already a rendition definition with the same name. If this is not the case, the system persists the rendition definition and creates the rendition.

On any properties update, the system will update the rendition. If you added the log statement to the log properties file, you can trace the rendition engine’s behaviour.

You can check the rendition by locating the document in the node browser. By default the rendition is stored as a hidden child node of the source document.

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

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.

Add an XML to PDF Transformer to Alfresco

Alfresco provides the ability to add custom transformers to transform a given source document to a different document format. There are several transformers available out of the box, but there is no transformer that let’s you transform an XML document to PDF based on XSL-FO. 

You can check the available transformers in Alfresco. Since version 3.4 there is an Administration Web Script (http://localhost:8080/alfresco/service/mimetypes) that lists all mimetypes and the transformers between these mimetypes.

Custom Transformer

You can write the transformer using Java, but you can also add a custom transformer by adding a couple of configuration files. It might not provide you with all the bells and whistles you can think of, but at least it shows how you easily add XSL-FO support without writing any code.

The transformer allows you to transform an XSL-FO document to PDF using a Run Action, a Content Rule or using the following line in a JavaScript:

document.transformDocument("application/pdf");

An XSL-FO document is an XML file containing the content that should be published in the resulting document combined with lay-out instructions. The lay-out instructions are somewhat similar to applying a CSS stylesheet to an HTML document. In Alfresco you can use a Freemarker template or an XSLT stylesheet to transform a source node into an XSF-FO document.

Download and install FOP

The first step is to download and install FOP, the apache XSL-FO processor. You can find it at http://xmlgraphics.apache.org/fop/. The next thing you need to do is to add it to the path to allow Alfresco to locate the executable files. Using Windows you can do this by adding the path to FOP to the PATH environment variable.

Configure the Transformer

The next step is to configure the transformer. Create a file custom-content transformer-context.xml in the folder tomcat/shared/classes/alfresco/extension and add the following lines:

<?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.worker.FOP.PDF" 
    class="org.alfresco.repo.content.transform.RuntimeExecutableContentTransformerWorker">
    <property name="mimetypeService">
      <ref bean="mimetypeService" />
    </property>
    <property name="checkCommand">
      <bean class="org.alfresco.util.exec.RuntimeExec">
        <property name="commandsAndArguments">
          <map>
            <entry key=".*">
              <list>
                <value>fop.bat</value>
                <value>-version</value>
              </list>
            </entry>
          </map>
        </property>
      </bean>
    </property>
    <property name="transformCommand">
      <bean class="org.alfresco.util.exec.RuntimeExec">
        <property name="commandsAndArguments">
          <map>
            <entry key=".*">
              <list>
                <value>fop.bat</value>
                <value>${source}</value>
                <value>${target}</value>
              </list>
            </entry>
          </map>
        </property>
        <property name="errorCodes">
          <value>1,2</value>
        </property>
      </bean>
    </property>
    <property name="explicitTransformations">
      <list>
        <bean class="org.alfresco.repo.content.transform.ExplictTransformationDetails">
          <property name="sourceMimetype">
        <value>text/xsl</value>
        </property>
          <property name="targetMimetype">
        <value>application/pdf</value>
      </property>
        </bean>
      </list>
    </property>
  </bean>
  <bean id="transformer.FOP.PDF" 
    class="org.alfresco.repo.content.transform.ProxyContentTransformer" 
    parent="baseContentTransformer">
    <property name="worker">
      <ref bean="transformer.worker.FOP.PDF" />
    </property>
  </bean>
</beans>

I the configuration above I only added the Windows command-line. If you run on Linux, you should remove the .bat extension.

Add the Mimetype

In order to allow Alfresco to recognize the XSL-FO source document, you need to add a new mimetype to Alfresco. There is no official mimetype for XSL-FO, but FOP lists an unregistered one called text/xsl. First add a file mimetypes-extension.xml in the Alfresco extension folder and add the following lines to the file:

<alfresco-config area="mimetype-map">
  <config evaluator="string-compare" condition="Mimetype Map">
    <mimetypes>
      <mimetype mimetype="text/xsl" display="XSL-FO">
        <extension>fo</extension>
      </mimetype>
    </mimetypes>
  </config>
</alfresco-config>

Next add a file custom-mimetype-map-context.xml to the extension folder in order to load the new mimetype during the system bootstrap.

<?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="mimetypeConfigService"
  class="org.springframework.extensions.config.xml.XMLConfigService"
  init-method="init">
    <constructor-arg>
      <bean class="org.springframework.extensions.config.source.UrlConfigSource">
        <constructor-arg>
          <list>
            <value>classpath:alfresco/mimetype/mimetype-map.xml</value>
            <value>classpath:alfresco/mimetype/mimetype-map-openoffice.xml</value>
            <value>classpath:alfresco/extension/mimetypes-extension.xml</value>
          </list>
        </constructor-arg>
      </bean>
    </constructor-arg>
  </bean>
</beans>

The last step is to add the transformer to the web-client-config-custom.xml:

<config evaluator="string-compare" condition="Action Wizards">
  <subtypes>
  </subtypes>
  <specialise-types>
  </specialise-types>
  <aspects>
  </aspects>
  <transformers>
    <transformer name="text/xsl" />
  </transformers>		
</config>

Restart Alfresco

When you restart Alfresco you should now be able to find the mimetype text/xsl listed in the Administration Web Script mentioned earlier. When you go to the details you should see that it is transformable to application/pdf.

Test the transformer

Now add a file called hello-world.fo to Alfresco with the following content:

<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4-portrait"
      page-height="29.7cm" page-width="21.0cm" margin="2cm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4-portrait">
    <fo:flow flow-name="xsl-region-body">
      <fo:block>Hello World!>/fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Go to the document’s detail view and click Run Action and select Transform and Copy Content. The required format shall be Adobe PDF Document. Once you click OK you should be able to find a document called hello-world.pdf in the selected destination space.

JavaScript example

You can use the content transformer action in JavaScript to transform an XSL-FO document to PDF:

var action = actions.create("transform"); 
action.parameters["destination-folder"] = document.parent; 
action.parameters["assoc-type"] = "{http://www.alfresco.org/model/content/1.0}contains"; 
action.parameters["assoc-name"] = document.name + "transformed"; 
action.parameters["mime-type"] = "application/pdf"; 
action.execute(document);

Creating XSL-FO output

There are a couple of options to create XSL-FO output in Alfresco. If you have XML documents that need to be transformed to PDF you can either use a Freemarker template or an XSLT stylesheet. You can use Alfresco’s XSLT Rendition Service to transform XML documents using XSLT.

Freemarker example

Create a file hello_world_fo.ftl in the space Data Dictionary/Presentation Templates and add the folowing lines to the file:

<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4-portrait"
      page-height="29.7cm" page-width="21.0cm" margin="2cm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4-portrait">
    <fo:flow flow-name="xsl-region-body">
      <fo:block>${document.name}</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Next create a file hello-world.js in Data Dictionary/Scripts and add the following content:

var template = companyhome.childByNamePath("/Data Dictionary/Presentation Templates/hello_world_fo.ftl");

if (template != null) {
  var result = document.processTemplate(template);

  var parent = document.parent;
  var name = document.name.replace(".txt",".fo");

  var output = parent.childByNamePath(name);

  if (output ==null) {
    output = parent.createFile(name);
  }
  output.content = result;

  output.transformDocument("application/pdf");
}

When you execute the script using a Run Action on a document it creates a PDF file with the document name in the same space as the source document.

Reformat Rendering Engine

As with all transformations you can also use the XSL-FO transformer to create a rendition using the Reformat Rendering Engine. The following JavaScript code snippet provides an example:

var renditionDef = renditionService.createRenditionDefinition('cm:pdfRenditionDef', 'reformat');
renditionDef.parameters['mime-type'] = 'application/pdf';
var pdfRendition= renditionService.render(document, renditionDef);

This code will create a PDF rendition for the given XSL-FO document.

Using XSLT

You can use Alfresco’s rendition service to transform documents using XSLT in Alfresco. Using JavaScript you can for example transform a document with code similar to this:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/hello-world.xsl");
var renditionDef = renditionService.createRenditionDefinition("cm:xsltRenditionDef", "xsltRenderingEngine");
renditionDef.parameters["template_node"] = template;
renditionDef.parameters["mime-type"] = "text/xsl";

var doc = renditionService.render(document, renditionDef);

You can then extend the rendition with a transformation to PDF using the Reformat Rendering Engine:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/foptestxsl.xsl");

var renditionDef = renditionService.createRenditionDefinition("vlc:xslRenditionDef", "xsltRenderingEngine");
renditionDef.parameters["template_node"] = template;
renditionDef.parameters["mime-type"] = "text/xsl";

var doc = renditionService.render(document, renditionDef);

// add a destination to make the rendition visible
var destination = document.displayPath + "/" + document.name + "-rendered.pdf";

renditionDef = renditionService.createRenditionDefinition("cm:foRenditionDef", "reformat");
renditionDef.parameters['mime-type'] = "application/pdf";
renditionDef.parameters["destination-path-template"] = destination;

var pdfRendition= renditionService.render(doc, renditionDef);

This example first creates a rendition that applies an XSLT stylesheet to an XML document to produce an XSL-FO document and then creates a reformat rendition to transform the result to PDF using our Apache FOP based custom transformer.

Composite Rendering Engine

There is also a Composite Rendering Engine that is able to just persist the PDF which is actually what you want to do in this case. For example:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/foptestxsl.xsl");

// the following method is not supported in the JavaScript renditionService
var compositeDefinition = renditionService.createCompositeRenditionDefinition("cm:compositeDefinition");

var xslRenditionDef = renditionService.createRenditionDefinition("cm:xslRenditionDef", "xsltRenderingEngine");
xslRenditionDef.parameters["template-node"] = template;
xslRenditionDef.parameters["mime-type"] = "text/xsl";

pdfRenditionDef = renditionService.createRenditionDefinition("cm:pdfRenditionDef", "reformat");
pdfRenditionDef.parameters['mime-type'] = 'application/pdf';

compositeDefinition.addAction(xslRenditionDef);
compositeDefinition.addAction(pdfRenditionDef);

var pdfRendition = renditionService.render(document, compositeRenditionDef);

This will not work as the Composite Rendering Engine does not seem to be exposed in the JavaScript API. The purpose of this post was to demonstrate what you can do without having to code Java. In a follow-up post I will show how to do this in Java.

Follow-up post

Read the follow-up post Using Alfresco Composite Rendition to Render PDF for examples how to use the Composite Rendition in Java and how to add the custom transformer in Java to use the FOP library already available in Alfresco in stead of having a separate installation of Apache FOP on the server.

Last updated: October 12, 2011