Skip navigation
All Places > Alfresco Content Services (ECM) > Blog
1 2 3 Previous Next

Alfresco Content Services (ECM)

416 posts

It has been a trending requirement in the Alfresco Digital Process Workspace (DPW) world to upload content to the ACS repository and bypass the APS process data. The following design-pattern provides an optimal solution to this requirement.

Components involved in this design-pattern

  • Alfresco Process Services (powered by Activiti) (Version 1.9 or above) 
  • Alfresco Content Services (Version 6.1 or above)
  • Application Development Framework (Version 2.6 or above)

ACS Implementation Steps
1. Login to ACS.
2. Create ACS Folder Template (Space Template) by navigating to Repository > Data Dictionary > Space Templates (Follow steps in
3. Note the `node id` of the Space Template.


APS Implementation Steps

1. Import the app into APS.
2. The Process Flow diagram is as below.
Process Diagram

3. Publish/Deploy the APS App.


ADF Implementation Steps

Customize the ADF code.


1. Add Custom code in create-process.component.ts to invoke the Space Template
createFolderFromTemplate(folderName: string, processId: string) {
const url = 'slingshot/doclib/folder-templates';
const localthis = this;
executeWebScript('POST', url, null, 'alfresco', 's', this.getRequestBody(folderName)).then(function (data) {
const localData: any = data;
localthis.updateProcessDetails(localData.persistedObject.split('Store/')[1], processId);
}, function (error) {
console.log('Error' + error);
2. Update callback function backFromProcessCreation() to invoke createFolderFromTemplate()
backFromProcessCreation(event: any): void {
3. Add custom HTML code to task-details-container.component.html


<div class="dw-template-container">
<apw-task-toolbar id="apw-task-toolbar-id"
[fileName]="'Task Audit -' + taskDetails?.id"

<div class="dw-template-content-container">
<div class="dw-template-content">
<div class="dw-template-fixed-content" fxHide.xs="{{showInfoDrawer}}">
<ng-container *ngIf="isDetailsTabActive(); else task_activity">

<divstyle="padding-top:0px;" class="dw-template-fixed-content" fxHide.xs="{{showInfoDrawer}}">

<adf-info-drawer title="">

<!-- Task Details -->
<adf-info-drawer-tab label="Task Details">
<apw-task-form id="apw-task-form-id"


<!-- Case Overview -->
<adf-info-drawer-tab label="Case Overview" class="adf-tabs-drawer">


<!-- Documents -->
<adf-info-drawer-tab label="Documents">
<div align="right" >


<divstyle="display: flex">
<divstyle="flex: 0 0 70% ">
[parentId]="nodeId" (success)="refreshDocList($event)" >

(preview)="showPreview($event)" >






<divstyle="height:100vh; padding-left: 10px;padding-right: 10px; flex: 0 0 20%" >

<divstyle="padding-top:10px" >
<adf-info-drawer [title]="'Comments'">
<adf-info-drawer-tab [label]="'Comments'">

<adf-info-drawer [title]="'Details'">
<adf-info-drawer-tab [label]="'Comments'">


<!-- Case Notes -->
<adf-info-drawer-tab label="Case Notes">


<ng-template #task_activity>
<apw-task-attachment id="apw-task-attachment-id"

<ng-container *ngIf="showInfoDrawer && hasTaskDetails()">
<div class="dw-template__sidebar">
<div class="dw-template-fixed-content">
<apw-task-sidebar id="apw-task-sidebar-id"

<adf-viewer #quickPreview




Run the DEMO







Alfresco provides Content REST API for downloading the content from repository as given below.


GET /alfresco/service/api/node/content/{store_type}/{store_id}/{id}?a=false&alf_ticket={TICKET}




a: attach. if true, force download of content as attachment. Possible values are true/false
store_type: workspace
store_id: SpacesStore
id: nodeRefId (UUID of the node)

A StoreRef is comprised of:


Store Protocol - that is, the type of store
Store Identifier - the id of the store


Example storeRefs are:


workspace://SpacesStore  (store_type://store_id)
version://versionStore  (store_type://store_id)
archive://SpacesStore  (store_type://store_id)
See here for Content API Webscript definition: content.get.desc.xml

Any external system or client can use this API to download content from Alfresco repository via an authenticated user which would be registered in Alfresco. 

If you want to know how to get auth ticket (alf_ticket) then visit: Alfresco Login REST API (


OOTB Download REST API will allow to download the content to any user who is registered in alfresco, since every user has consumer access to every site by default via "EVERYONE" group. But let's suppose you want to put some kind of restrictions to the Download API. Let's say for example:

1- Allow download if requesting user is authorized to download the content.
2- Want to validate the site level user role e.g. only Managers/Collaborators/Contributors can download, Consumers should not be allowed to download.
3- Want to check if user is part of DOWNLOADERS groups then allow them to download 
and so on. 
There could be many such cases which we can not achieve via OOTB REST API provided by alfresco. If your contents has copyrights you will definitely not allow users to download the content who are unauthorized. 
To handle such scenarios you need to write a custom Download webscript.
Alfresco provides a Webscript called "org.alfresco.repo.web.scripts.content.StreamContent" ( 

By extending this class we can add our custom user validation logic and leave the streaming and download handling part to this OOTB Webscript.
So, let's take a use case where you don't want a consumer user to download the content from a site. To achieve this use case a custom webscript will be written as given below:


package com.github.abhinavmishra14.webscript;

import java.util.Locale;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.web.scripts.content.StreamContent;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

* The Class DownloadContentWebscript.

public class DownloadContentWebscript extends StreamContent {

/** The Constant LOGGER. */
private static final Logger LOGGER = LoggerFactory.getLogger(DownloadContentWebscript.class);

/** The content service. */
private ContentService contentService;

/** The authentication service. */
private AuthenticationService authenticationService;

/** The site service. */
private SiteService siteService;

/** The authority service. */
private AuthorityService authorityService;

/* (non-Javadoc)
  * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse)

public void execute(final WebScriptRequest request,
   final WebScriptResponse response) throws IOException {"Started executing DownloadContentWebscript...");
  try {
   final NodeRef nodeRef = getParameterAsNodeRef(request, "nodeRef");
   final String userName = authenticationService.getCurrentUserName();
   if(isNotAuthorised(nodeRef, userName, siteService, permissionService, authorityService)) {
    response.getWriter().write("User is unauthorised to download the requested content!");
   } else {
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Processing the download requested by: {}", userName);
    final boolean attach = Boolean.valueOf(request.getParameter("attach"));
    processDownload(request, response, nodeRef, attach, ContentModel.PROP_CONTENT);
  } catch (AccessDeniedException accessDenied) {
   LOGGER.error("Access denied while downloading content", accessDenied);
   throw new WebScriptException(Status.STATUS_UNAUTHORIZED,
     accessDenied.getMessage(), accessDenied);
  } catch (IOException | AlfrescoRuntimeException
    | InvalidNodeRefException excp) {
   LOGGER.error("Exception occurred while downloading content", excp);
   throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,
     excp.getMessage(), excp);
  }"Existing from DownloadContentWebscript...");

  * Process download.
  * @param request the request
  * @param response the response
  * @param nodeRef the node ref
  * @param attach the attach
  * @param propertyQName the property q name
  * @throws IOException the IO exception

private void processDownload(final WebScriptRequest request,
   final WebScriptResponse response, final NodeRef nodeRef, final boolean attach,
   final QName propertyQName) throws IOException {
  String userAgent = request.getHeader("User-Agent");
  userAgent = StringUtils.isNotBlank(userAgent) ? userAgent.toLowerCase(Locale.ENGLISH) : StringUtils.EMPTY;
  final boolean isClientSupported= userAgent.contains("msie")
    || userAgent.contains(" trident/")
    || userAgent.contains(" chrome/")
    || userAgent.contains(" firefox/");

  if (attach && isClientSupported) {
   String fileName = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
   if (userAgent.contains("msie") || userAgent.contains(" trident/")) {
    final String mimeType = contentService.getReader(nodeRef, propertyQName).getMimetype();
    if (!(this.mimetypeService.getMimetypes(FilenameUtils.getExtension(fileName)).contains(mimeType))) {
     fileName = FilenameUtils.removeExtension(fileName)+ FilenameUtils.EXTENSION_SEPARATOR_STR
       + this.mimetypeService.getExtension(mimeType);
   streamContent(request, response, nodeRef, propertyQName, attach, fileName, null);
  } else {
   streamContent(request, response, nodeRef, propertyQName, attach, null, null);

     * Create NodeRef instance from a WebScriptRequest parameter.
     * @param req the req
     * @param paramName the param name
     * @return the parameter as node ref

    private NodeRef getParameterAsNodeRef(final WebScriptRequest req, final String paramName) {
        final String nodeRefStr = StringUtils.trimToNull(req.getParameter(paramName));
        if (StringUtils.isBlank(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing " + paramName + " parameter");
        if (!NodeRef.isNodeRef(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Incorrect format for " + paramName + " paramater");
        final NodeRef nodeRef = new NodeRef(nodeRefStr);
        if (!nodeService.exists(nodeRef)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, paramName + " not found");
        return nodeRef;
  * Checks if is not authorised.
  * @param nodeRef the node ref
  * @param userName the user name
  * @param siteService the site service
  * @param permissionService the permission service
  * @param authorityService the authority service
  * @return true, if checks if is not authorised

private boolean isNotAuthorised(final NodeRef nodeRef,
   final String userName, final SiteService siteService,
   final PermissionService permissionService,
   final AuthorityService authorityService) {
  boolean isNotAuthorised = false;
  final SiteInfo siteInfo = siteService.getSite(nodeRef);
  // Checking siteInfo, If it is null that means user is not a member of site and
  // hence isNotAuthorised is default to false.
  if (null != siteInfo) {
   if (siteService.isMember(siteInfo.getShortName(), userName)) {
    final Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Checking isNotAuthorised, Available access permissions are: {}", permissions);
    for (final AccessPermission permission : permissions) {
     if (permission.getPermission().equals("SiteConsumer")
       || permission.getPermission().equals("Consumer")) {
      if (permission.getAuthorityType().equals("USER")
        && permission.getAuthority().equals(userName)) {
       isNotAuthorised = true;
      } else if (permission.getAuthorityType().toString().equals("GROUP")) {
       //Set run as system user since other users including consumers can not fetch authorities
       final Set<String> authorities = authorityService.getAuthoritiesForUser(userName);
       //Clear system user context and set original user context
       if(LOGGER.isDebugEnabled()) {
        LOGGER.debug("Checking permissions at GROUP level, user has following authorities: {}", authorities);
       for (final String authority : authorities) {
        if (authority.equals(permission.getAuthority())) {
         isNotAuthorised = true;
   } else {
    isNotAuthorised = true;//Not a member in the site.
  return isNotAuthorised;

  * Sets the content service.
  * @param contentService the content service

public void setContentService(final ContentService contentService) {
  this.contentService = contentService;

  * Sets the authentication service.
  * @param authenticationService the authentication service

public void setAuthenticationService(final AuthenticationService authenticationService) {
  this.authenticationService = authenticationService;

  * Sets the site service.
  * @param siteService the site service

public void setSiteService(final SiteService siteService) {
  this.siteService = siteService;

  * Sets the authority service.
  * @param authorityService the authority service

public void setAuthorityService(final AuthorityService authorityService) {
  this.authorityService = authorityService;

WebScript Description:


<shortname>Download Content</shortname>
      Download Content based on role and permissions check.
      nodeRef- NodeRef of the content e.g. workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e
      attach (Optional)- if true, force download of content as attachment (Possible values are true/false)
      Sample URL:

<format default="">argument</format>
<transaction allow="readonly" />


Bean definition:


<bean id="" class="com.github.abhinavmishra14.webscript.DownloadContentWebscript" parent="webscript">
  <property name="permissionService" ref="PermissionService" />
  <property name="nodeService" ref="NodeService" />
  <property name="mimetypeService" ref="MimetypeService" />
  <property name="delegate" ref="webscript.content.streamer" />
  <property name="repository" ref="repositoryHelper" />
  <property name="contentService" ref="ContentService" />
  <property name="authenticationService" ref="AuthenticationService" />
  <property name="siteService" ref="SiteService" />
  <property name="authorityService" ref="AuthorityService"/>

Sometimes an Alfresco Administrator might need to track active logged-in users in the system for audit purpose or for planning maintenance activities. There can be many other use-cases depending of type of usage or organization policies.


Alfresco doesn't provide this kind of feature for admins out of the box as of now. However Alfresco 5.2 version on-wards they are providing Support Tools feature which can provide various options including this particular use-case.


Support Tools is an Add-On, which is available on GitHub called alfresco-support-tools which can be installed as a module as well. It seems Alfresco 5.2 on-wards alfresco has integrated this add-on OOTB. I have not tried this add-on so not sure how it works. However the screenshots given at add-on page shows that it has good amount of features which is really useful for admins.


See here: Support Tools


Here we are going to use Alfresco's TicketComponent service to get the active user details and active ticket details.
We will create a java backed web-script which will return the basic details about the active users, total active user count, total no. of active tickets etc.


Follow the below given steps:


1-      Create alfresco webscript descriptor “getActiveUsers.get.desc.xml


  <shortname>Active Users</shortname>

   <description>This webscript returns the active users logged-in into Alfresco.

Sample response:


  activeTicketsCount: 2,

  activeUsers: "["admin"," "]",

  activeUsersCount: 2,

  _comment: "Active user count may be lower than the ticket count, since a user can have more than one ticket/session. Ticket count may be higher than the active user count, since a user can have more than one ticket/session."




  <format default="json"/>





2-      Create a freemarker template “getActiveUsers.get.json.ftl” which is used to generate the view


<#escape x as jsonUtils.encodeJSONString(x)> ${response} </#escape>


3-      Create a Java webscript class “

package com.github.abhinavmishra14.audit.webscript;


import java.util.Map;

import java.util.Set;

import java.util.concurrent.ConcurrentHashMap;


import org.alfresco.error.AlfrescoRuntimeException;


import org.json.JSONException;

import org.json.JSONObject;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.extensions.webscripts.Cache;

import org.springframework.extensions.webscripts.DeclarativeWebScript;

import org.springframework.extensions.webscripts.Status;

import org.springframework.extensions.webscripts.WebScriptException;

import org.springframework.extensions.webscripts.WebScriptRequest;


import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;



* The Class GetActiveUsersWebscript.


public class GetActiveUsersWebscript extends DeclarativeWebScript {


/** The Constant LOGGER. */

private static final Logger LOGGER = LoggerFactory.getLogger(GetActiveUsersWebscript.class);


/** The Constant ACTIVE_USERS. */

private static final String ACTIVE_USERS = "activeUsers";


/** The Constant ACTIVE_USERS_COUNT. */

private static final String ACTIVE_USERS_COUNT = "activeUsersCount";


/** The Constant ACTIVE_TICKETS_COUNT. */

private static final String ACTIVE_TICKETS_COUNT = "activeTicketsCount";


/** The Constant COMMENT_DATA. */

private static final String COMMENT_DATA = "_comment";


/** The Constant COMMENT. */

private static final String COMMENT = "getActiveUsers.comment";


/** The Constant RESPONSE. */

private static final String RESPONSE = "response";


/** The ticket component. */

private final TicketComponent ticketComponent;



* The Constructor.


* @param ticketComponent the ticket component


public GetActiveUsersWebscript(final TicketComponent ticketComponent) {


  this.ticketComponent = ticketComponent;




public Map<String, Object> executeImpl(final WebScriptRequest req,

                final Status status, final Cache cache) {

final Map<String, Object> model = new ConcurrentHashMap<String, Object>(3);

  try {

                //get nonExpiredOnly users with tickets

                final Set<String> activeUsers = ticketComponent.getUsersWithTickets(true);

                final ObjectMapper objMapper = new ObjectMapper();



                if (activeUsers != null && !activeUsers.isEmpty()) {

                  final JSONObject activeUsersJson = new JSONObject();

                //This may be lower than the ticket count, since a user can have more than one

              // ticket/session

                 activeUsersJson.put(ACTIVE_USERS, objMapper.writeValueAsString(activeUsers));

                activeUsersJson.put(ACTIVE_USERS_COUNT, activeUsers.size());


                //This may be higher than the user count, since a user can have more than one   

               // ticket/session

                //get nonExpiredOnly ticket count

                activeUsersJson.put(ACTIVE_TICKETS_COUNT, ticketComponent.countTickets(true));


                activeUsersJson.put(COMMENT_DATA, "Active user count may be lower than the ticket count, since a user can have more than one ticket/session. Ticket count may be higher than the active user count, since a user can have more than one ticket/session.");

                model.put(RESPONSE, activeUsersJson);


    } catch (JsonProcessingException | JSONException excp) {

                LOGGER.error("Exception occurred while preparing json for active users ", excp);

                throw new WebScriptException(

                  Status.STATUS_INTERNAL_SERVER_ERROR, excp.getMessage(), excp);

    } catch (AlfrescoRuntimeException alfErr) {

                LOGGER.error("Unexpected error occurred while getting active users ", alfErr);

                throw new WebScriptException(

                  Status.STATUS_INTERNAL_SERVER_ERROR,     alfErr.getMessage(), alfErr);


    return model;




4-      Add the bean definition for the java webscript in spring context file.

<bean id=""

class=" com.github.abhinavmishra14.audit.webscript.GetActiveUsersWebscript" parent="webscript">

    <constructor-arg ref="ticketComponent"/>



5-      Build and start the alfresco instance.

6-      Access the service using URL: (You will be prompted for credentials, note that the user should be admin who is accessing this service)

7-      It will return following type of response:


 activeTicketsCount: 3,

 activeUsers: "["admin","test"," "]",

 activeUsersCount: 3,

 _comment: "Active user count may be lower than the ticket count, since a user can have more than one ticket/session. Ticket count may be higher than the active user count, since a user can have more than one ticket/session."


We can inject the repository service dependencies using 2 methods. One method is a lower case e.g. “contentService” bean ref and other method is upper case e.g. “ContentService” bean ref, similarly we can use "nodeService" or "NodeService".

To use a service in your custom implementation, you usually use a spring config (spring context) file to inject service bean dependencies. So in your spring context file, for your class for example “com.abhinav.CustomAction”, you can either use "nodeService" [lower-case] or "NodeService" [upper-case].


 Method 1-

   <bean id="customAction" class="com.abhinav.CustomAction">

      <property name="nodeService">

          <!-- Lower case -->

          <ref bean="nodeService"/>


      <property name="contentService">

          <!-- Lower case -->

          <ref bean="contentService"/>




Method 2-

<bean id="customAction" class="com.abhinav.CustomAction">

      <property name="nodeService">

          <!-- Lower case -->

          <ref bean="NodeService"/>


      <property name="contentService">

          <!-- Lower case -->

          <ref bean="ContentService"/>




The best practice is that we should always use services with upper case “NodeService”, “ContentService”, “FileFolderService” etc. because this lower case “nodeService”, ”contentService” etc.  bypasses security check, audit and transaction checks.

Technically Alfresco uses AOP (Aspect-Oriented Programming) to expose services as “AOP proxies”.



To prove that, let’s go through publicly exposed services and some core services such as nodeService and contentService:





<bean id="contentService" parent="baseContentService">

                <property name="store">

                   <ref bean="fileContentStore"/>





<bean id="nodeService" class="org.springframework.aop.framework.ProxyFactoryBean">

                <!--  Lazy init to avoid circular dependencies  -->

                <property name="targetSource">

                                <bean class="org.alfresco.config.NonBlockingLazyInitTargetSource">

                                                <property name="targetBeanName">

                                                                <idref bean="_nodeService"/>




                <property name="proxyInterfaces">






Note: you can look into the xml files for more details.


<!--  Public Node Service  -->

<bean id="NodeService" class="org.springframework.aop.framework.ProxyFactoryBean">

                <property name="proxyInterfaces">





                <property name="target">

                                <ref bean="nodeService"/>


                <property name="interceptorNames">


                                                <idref local="NodeService_transaction"/>

                                                <idref local="AuditMethodInterceptor"/>

                                                <idref local="exceptionTranslator"/>

                                                <idref bean="NodeService_security"/>

                                                <idref bean="disableAuditablePolicySetPropertyInterceptor"/>





<!--  Public Content Service  -->

<bean id="ContentService" class="org.springframework.aop.framework.ProxyFactoryBean">

                <property name="proxyInterfaces">



                <property name="target">

                                <ref bean="contentService"/>


                <property name="interceptorNames">


                                                <idref local="ContentService_transaction"/>

                                                <idref local="AuditMethodInterceptor"/>

                                                <idref local="exceptionTranslator"/>

                                                <idref bean="mlContentInterceptor"/>

                                                <idref bean="ContentService_security"/>





Notice that, the property ‘interceptorNames’ which has list of interceptors, which force the execution of transaction check (ContentService_transaction), audit (AuditMethodInterceptor) and security check (ContentService_security) etc.


So when you directly call the “contentService” (lower case), all these check are bypassed.

This can leads to security issue because alfresco will not evaluate the security before running the corresponding service operation in this case. So, it is best practice to use upper case services always.

Sometimes we need to perform some tasks or operations on regular basis or asynchronously after a certain time interval. It could be archival or transformation of contents or processing of jobs in Alfresco repository.


Another use case would be for example there is a third party system which is integrated with Alfresco and produces some output or transform contents for Alfresco, In this case you can’t rely on a synchronous call to the services on third party system. If the third party system gets hung or takes time to process the request your alfresco request will result into time out or failure. So in this case you can create job objects for each request into Alfresco inside some folder (job node) and let them processed by the job scheduler asynchronously. 


Job scheduler will fetch all jobs which are queued in job folder (job node) and process them. It keeps checking the status of the request placed at third party system every minute or 5 minute based on the scheduler configuration and do some operation once third party system returns desired response.

The simplest way would be to use Quartz framework with Cron. We will see how to create a simple job scheduler using the quartz and cron.


You can look the Chapter 22 in spring documentation and Quartz Enterprise documentation. Quartz and Cron are explained here.


I have used ‘org.quartz.StatefulJob’ to create a simple scheduled job in Alfresco. Let’s see how to implement it.


Before that you need to implement the Action/Webscript which will actually create job objects at some location (job folder) in alfresco repository. Scheduler will fetch these jobs and process them.


Let say there is an Action in alfresco using which user triggers transformation of a big XML document on a third party system.  


Assume following statements are in place in order to implement scheduled job:


Ø  Alfresco action invokes transformation on third party system

Ø  The third party system sends the response immediately with current transformation status say ‘Created’ and a unique id (e.g. transaction id).

Ø  Alfresco action reads the response and based on that it creates a content less job object in alfresco repository with the help of content model (content model defines an aspect and some properties to hold the transformation status, unique id, action requester user name/email, date, time, etc. ) and apply the properties to job object.

Ø  Scheduled job in Alfresco will fetch all job objects and keep checking the status of the transformation using the unique id (saved as part of job object) provided by third party system.



We will not go into much detail about how to create the job object. Above statements are more than enough for a developer to understand how to create job object, isn’t it J


Now it’s time to look at the actual topic which we are talking about here.




Follow the below given steps to implement a job scheduler using org.quartz.StatefulJob:


1- Create custom-scheduled-job-context.xml where we define job detail and job trigger bean


<bean id="customJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">

<property name="jobClass">



 <property name="jobDataAsMap">


 <entry key="serviceRegistry">

   <ref bean="ServiceRegistry"/>


 <entry key="globalProperties">

   <ref bean="global-properties"/>








-- jobClass has a value ‘’. This class holds the actual job processing logic. 


-- The jobDataAsMap can be used to hold any number of (serializable) objects which you wish to have made available to the job instance when it executes. It is an implementation of the Java Map interface, and in jobDataAsMap you can pass alfresco services which will be required by your job to access repository.


-- org.springframework.scheduling.quartz.JobDetailBean 


It is subclass of Quartz' JobDetail class that eases bean-style usage. JobDetail itself is already a JavaBean but lacks sensible defaults.  


See more details here: JobDetailBean 



<bean id="customJobTrigger" class="org.alfresco.util.CronTriggerBean">

        <property name="jobDetail">

            <ref bean="customJobDetail"/>


        <property name="scheduler">

            <ref bean="schedulerFactory"/>


        <property name="cronExpression">

          <!-- Run the job every 1 minutes -->

          <value>0 0/1 * 1/1 * ? *</value>






-- jobDetail has a bean reference which we have defined above, it injects the job configuration defined above to org.alfresco.util.CronTriggerBean.


-- scheduler has a bean reference ‘schedulerFactory’, it injects the schedulerFactory (instance of org.quartz.Scheduler) to org.alfresco.util.CronTriggerBean


-- cronExpression value in customJobTrigger defines the time interval at which this job will be executed


-- org.alfresco.util.CronTriggerBean class is defined in Alfresco utility which extends ‘org.alfresco.util.AbstractTriggerBean’ and AbstractTriggerBean is the implementation of org.springframework.scheduling.quartz.JobDetailAwareTrigger’ 



See more details here:  CronTriggerBean


2- Import “custom-scheduled-job-context.xml” into module-context.xml if using amp, else place it inside <tomcat>/shared/classes/alfresco/extension


3- Now we have created custom Scheduled Job called customJobDetail and associating it with Job trigger called customJobTrigger.


4- Now, Create job class CustomJobProcessor  under package and implements org.quartz.StatefulJob


5- We need to implement execute (signature given below) method and under that whole business logic will reside. 


public void execute(final JobExecutionContext jobCtx) throws JobExecutionException;




* Author: Abhinav Kumar Mishra

* Copyright &COPY; 2015 Abhinav Kumar Mishra. All rights reserved.





import org.alfresco.service.ServiceRegistry;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.quartz.JobExecutionContext;

import org.quartz.JobExecutionException;



 * The Class CustomJobProcessor.<br/>


public class CustomJobProcessor implements org.quartz.StatefulJob{

/** The Constant LOG. */

private static final Log LOG = LogFactory.getLog(CustomJobProcessor.class);


/** The global properties. */

private Properties globalProperties;

/** The service registry. */

private ServiceRegistry serviceRegistry;



public void execute(final JobExecutionContext jobCtx) throws JobExecutionException {"CustomJobProcessor Started..");


     //Run as system user since this job is user independent hence

       // permission is required on repo


    // TODO:: Put job processing logic here..

    // Get the job space where all jobs are stored 

                  // using  serviceRegistry.getFileFolderService()

                  // Read the jobs in a list as given below:

                  // List<FileInfo> fileInfoList =    fileFolderService.listFiles(jobSpaceNode);

   // Read the uniuqe id from the job property and process

  } catch (RuntimeException excp){

     LOG.error("Exception occured while processing job", excp);

  }"CustomJobProcessor End!");

}// execute end



 * Sets the global properties.


 * @param globalProperties the global properties


public void setGlobalProperties(final Properties globalProperties) {

this.globalProperties = globalProperties;



 * Sets the service registry.


 * @param serviceRegistry the service registry


public void setServiceRegistry(final ServiceRegistry serviceRegistry) {

this.serviceRegistry = serviceRegistry;




6- Prepare the amp and apply to alfresco.war or prepare the jar file and place it under <tomcat>/webapps/alfresco/WEB-INF/lib


7- Restart the server and your scheduled job will be executed on defined time intervals. You can define the cron expression based on your requirements. Refer this guide to know more about cron expressions and cron maker.



A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the allowed values, along with various combinations of the allowed special characters for that field. The fields are as follows:


Field Name


Allowed Values

Allowed Special Characters




, - * /




, - * /




, - * /

Day of month



, - * ? / L W



1-12 or JAN-DEC

, - * /

Day of week


1-7 or SUN-SAT

, - * ? / L #



empty, 1970-2099

, - * /

So cron expressions can be as simple as this: * * * * ? *



So, we saw that how to implement a simple scheduled job. But suppose that your alfresco server is running in Clustered environment (this is likely in production environment). In this case there will be multiple instances of the same schedulers running which is nothing but the multiple threads. Now to handle this case Alfresco has implemented ‘org.alfresco.schedule.AbstractScheduledLockedJob


It makes the cluster aware locking of the job transparent to the implementation. 

On the job's spring JobExecutionContext it will still always have to be passed as parameter the JobLockService.


The name to be used for locking of the job is optional, If none is passed a name will be composed using the simple name of the implementation class.

In general if it may make sense to have more than one job setup using the same class you should always use a different name on each JobExecutionContext to differentiate the jobs, unless you want the lock to be shared between the different instances.


AbstractScheduledLockedJob (Cluster Aware Job Scheduler Implementation)


Follow the below given steps to implement a job scheduler using org.alfresco.schedule.AbstractScheduledLockedJob:


1- Create custom-cluster-aware-scheduled-job-context.xml where we define job detail and job trigger bean


  <bean id="customClusterAwareJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">



<property name="jobClass">



<property name="jobDataAsMap">


<entry key="fileFolderService">

  <ref bean="fileFolderService" />


<entry key="nodeService">

   <ref bean="nodeService" />


<entry key="transactionService">

 <ref bean="transactionService"/>


<!-- JobLockService used to aquire the lock on jobs while they are 

     being processed to avoid other thread to modify the jobs state-->

<entry key="jobLockService">

<ref bean="jobLockService"/>






  <bean id="customClusterAwareJobTrigger" class="org.alfresco.util.CronTriggerBean">



<property name="jobDetail">

<ref bean="customClusterAwareJobDetail" />


<property name="scheduler">

<ref bean="schedulerFactory" />


<property name="cronExpression">

    <!-- Provided the cron expession in alfresco-global.propeties file -->





2- Import “custom-cluster-aware-scheduled-job-context.xml” into module-context.xml if using amp, else place it inside <tomcat>/shared/classes/alfresco/extension


3- Now we have created custom Scheduled Job called customClusterAwareJobDetail and associating it with Job trigger called customClusterAwareJobTrigger.


4- Now, Create job class CustomClusterAwareJobProcessor under package and extend org.alfresco.schedule.AbstractScheduledLockedJob class.


5- We need to implement executeJob (signature given below) method and under that whole business logic will reside. 


public void executeJob(final JobExecutionContext jobCtx) throws JobExecutionException;



* Author: Abhinav Kumar Mishra

* Copyright &COPY; 2015 Abhinav Kumar Mishra. All rights reserved.





import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;

import org.alfresco.schedule.AbstractScheduledLockedJob;

import org.alfresco.service.cmr.model.FileFolderService;

import org.alfresco.service.cmr.repository.NodeService;

import org.alfresco.service.transaction.TransactionService;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.quartz.JobExecutionContext;

import org.quartz.JobExecutionException;



 * The Class CustomClusterAwareJobProcessor.<br/>


 * This class extends AbstractScheduledLockedJob to execute jobs using JobLockService. <br/>

 * It makes the cluster aware locking of the job transparent to the implementation. On the job's spring

 * JobExecutionContext it will still always have to be passed as parameter the

 * jobLockService.<br/> The name to be used for locking of the job is optional, if

 * none is passed a name will be composed using the simple name of the

 * implementation class. <br/>In general if it may make sense to have more than one

 * job setup using the same class you should always use a different name on each

 * JobExecutionContext to differentiate the jobs, unless you want the lock to be

 * shared between the different instances.<br/>


 * The only method to be implemented when extending this class is

 * executeJob(JobExecutionContext).


 * @see org.alfresco.schedule.AbstractScheduledLockedJob

 * @see org.alfresco.repo.lock.JobLockService


public class CustomClusterAwareJobProcessor  extends AbstractScheduledLockedJob{

/** The Constant LOG. */

private static final Log LOG = LogFactory.getLog(CustomClusterAwareJobProcessor .class);


/** The file folder service. */

private FileFolderService fileFolderService;


/** The node service. */

private NodeService nodeService;


/** The transaction service. */

private TransactionService transactionService;



 * This method will process the job by taking the lock on jobs. Since its an

 * extension of {@link org.alfresco.schedule.AbstractScheduledLockedJob} it

 * should also receive reference to the service

 * {@link org.alfresco.repo.lock.JobLockService}.


 * @param jobContext the job execution context

 * @throws JobExecutionException if there is an exception while executing the job.



public void executeJob(final JobExecutionContext jobCtx)

throws JobExecutionException {"CustomClusterAwareJobProcessor  Started..");

try {

// Run as system user since this job is user independent hence

// permission is required on repository




final RetryingTransactionCallback<Object> txnWork = 

                                                     new RetryingTransactionCallback<Object>() {



   public Object execute() throws Exception {

     // TODO:: Put job processing logic here..

      // Get the job space where all jobs are stored 

       // using  serviceRegistry.getFileFolderService()

      // Read the jobs in a list as given below:

       // List<FileInfo> fileInfoList = fileFolderService.listFiles(jobSpaceNode);

      // Read the uniuqe id from the job property and process

    return null;




} catch (RuntimeException excp) {

LOG.error("Exception occured while processing job", excp);

}"CustomClusterAwareJobProcessor End!");




 * Gets the file folder service.


 * @return the file folder service


public FileFolderService getFileFolderService() {

return fileFolderService;




 * Sets the file folder service.


 * @param fileFolderService the file folder service


public void setFileFolderService(final FileFolderService fileFolderService) {

this.fileFolderService = fileFolderService;




 * Gets the node service.


 * @return the node service


public NodeService getNodeService() {

return nodeService;




 * Sets the node service.


 * @param nodeService the node service


public void setNodeService(final NodeService nodeService) {

this.nodeService = nodeService;





 * Gets the transaction service.


 * @return the transaction service


public TransactionService getTransactionService() {

return transactionService;





 * Sets the transaction service.


 * @param transactionService the transaction service


public void setTransactionService(final TransactionService transactionService) {

this.transactionService = transactionService;




6- Prepare the amp and apply to alfresco.war or prepare the jar file and place it under <tomcat>/webapps/alfresco/WEB-INF/lib

7- Restart the server and your scheduled job will be executed on defined time intervals.

The release of the following versions includes Mutual TLS Authentication by Default for Alfresco Repository and SOLR communications:


This blog post describes the changes required to upgrade your installation if you were using SSL or HTTP. Also a review on how the communication between SOLR and Repository works is included, as these concepts help to understand the simplified deployment diagrams with discouraged and recommended configurations.


Exec Summary

  • Alfresco is being released from now using Mutual Authentication TLS between Alfresco Repository and SOLR by default
  • Before upgrading from Alfresco 6.0, configuration properties in Repository and SOLR must be reviewed, as default values changed from none to https
  • Docker Compose templates and Helm Charts must be also reviewed, as when using plain HTTP a new ALFRESCO_SECURE_COMMS environment variable is required to configure Alfresco Search Services and Alfresco Insight Engine images
  • Out of the box truststores and keystores have been removed and they must be generated from scratch using the new Alfresco SSL Generator
    • If you were using custom truststores and keystores, you can keep on using them when upgrading
  • If you were using an unsecure deployment, you must switch to any of the recommended scenarios


Calls to Action

This section provides a guide to upgrade your installation from different use cases:

  1. You were using Mutual Authentication TLS (SSL)
  2. You were using HTTP and you want to employ SSL as part of the upgrade
  3. You were using HTTP and you want to continue using HTTP after the upgrade


1. If you were using Mutual Authentication TLS (SSL)

If you were using Mutual Authentication TLS protocol for these communications, everything should work as before when using these new releases.


However, from these versions, default keystores and truststores have been removed from distribution ZIP files and Docker Images.


  • Repository provided the cryptographic stores inside the classpath, as part of the alfresco-repository JAR library.
  • Search Services provided the cryptographic stores in the configuration folder for the instance templates, that is copied to alfresco and archive core when created.


Since these resources are not available out of the box anymore, it's required to create a new set of cryptographic stores before upgrading your server or to perform a backup of your previous stores and a restoring process in the upgraded server. A new tool, named Alfresco SSL Generator, has been developed for this purpose. This tool can be used from Windows, Linux and Mac computers and it produces all the cryptographic stores required to configure the Mutual Authentication TLS protocol.

$ cd ssl-tool

$ ./

$ tree keystores/
├── alfresco
│ ├── keystore
│ ├──
│ ├──
│ ├──
│ ├── ssl.keystore
│ └── ssl.truststore
├── client
│ └── browser.p12
└── solr
├── ssl.repo.client.keystore
└── ssl.repo.client.truststore


Once the new stores have been created, you can copy them to Repository and SOLR configuration folders. Detailed steps for this process are available at:

If you were using the Docker Images, verify that your configuration is using the keystores and truststores from an external volume or extend default Docker Image to copy these cryptographic stores to the configured paths inside the container. A sample Docker Compose configuration is provided at Alfresco/alfresco-ssl-generator


Detailed information on how to use Alfresco Search Services Docker Image is available at Alfresco/SearchServices


2. If you were using HTTP and you want to switch to SSL

If you were using plain HTTP protocol for these communications but you want to use SSL after the upgrade, you must review your configuration to enable this protocol.



Inside file, check that following values are set.


Use Alfresco SSL Generator to produce all the cryptographic stores required to configure the Mutual Authentication TLS protocol.


Once the new stores have been created, you can copy them to Repository and SOLR configuration folders. Detailed steps for this process are available at Alfresco Content Services 6.1 > Installing the Alfresco WARS


Also Tomcat configuration for Repository needs to be reviewed, as a new SSL Connector for SOLR must be added. Detailed steps for this configuration are available at Alfresco Content Services 6.1 > Installing the Tomcat application server



Inside file (both "alfresco" and "archive" cores), following value must be set.



Additionally, Jetty server configuration and cryptographic stores paths must be set. Detailed steps for this configuration are available at Alfresco Search Services 1.3 > Installing and configuring Search Services with mutual TLS


If you were using the Docker Images, keystores and truststores must be set from an external volume or they should be copied to the configured paths inside the container. A sample Docker Compose configuration is provided at Alfresco/alfresco-ssl-generator


Detailed information on how to use Alfresco Search Services Docker Image is available at Alfresco/SearchServices


3. If you were using HTTP and you want to continue using HTTP

If you were using plain HTTP protocol for these communications, you must verify that you are explicitly using this configuration instead of relying on default values.



Inside file, following value must be set.


If you were using the Docker Image, verify that this setting is also set in your service declaration in docker-compose.yml file.

image: alfresco/alfresco-content-repository:
mem_limit: 1700m



Inside file (both "alfresco" and "archive" cores), following value must be set.


If you are using the Docker Image, a new environment variable named ALFRESCO_SECURE_COMMS is available. This variable accepts "none" for HTTP and "https" for Mutual TLS Authentication, so you should add this line to your service declaration in docker-compose.yml file.

    image: alfresco/alfresco-search-services:
        # Solr needs to know how to register itself with Alfresco
        - SOLR_ALFRESCO_HOST=alfresco
        - SOLR_ALFRESCO_PORT=8080
        # Alfresco needs to know how to call solr
        - SOLR_SOLR_HOST=solr6
        - SOLR_SOLR_PORT=8983
        # Create the default alfresco and archive cores
        - SOLR_CREATE_ALFRESCO_DEFAULTS=alfresco,archive
        - "SOLR_JAVA_MEM=-Xms2g -Xmx2g"
        # HTTP by default


Web Proxy

When using plain HTTP for communications between Repository and SOLR, some REST API services must be protected from external access, as only communications in the internal network are secure. Following snippet is a sample configuration for NGINX, but you can use any other Web Proxy for that.

# Protect access to SOLR APIs
location ~ ^(/.*/service/api/solr/.*)$ {return 403;}
location ~ ^(/.*/s/api/solr/.*)$ {return 403;}
location ~ ^(/.*/wcservice/api/solr/.*)$ {return 403;}
location ~ ^(/.*/wcs/api/solr/.*)$ {return 403;}
location ~ ^(/.*/proxy/alfresco/api/solr/.*)$ {return 403 ;}
location ~ ^(/.*/-default-/proxy/alfresco/api/.*)$ {return 403;}

Additional information for this task is available at Adding a reverse proxy in front of Alfresco Content Services.


We have included also this configuration as default for Docker Compose reference templates:


acs-deployment/docker-compose.yml at master · Alfresco/acs-deployment · GitHub 

Using alfresco/alfresco-acs-nginx:3.0.1 Docker Image for NGINX configuration.


acs-community-deployment/docker-compose.yml at master · Alfresco/acs-community-deployment · GitHub 

Using alfresco/acs-community-ngnix:1.0.0 Docker Image for NGINX configuration.


These are the main changes you'll find, but detailed information is provided below on why we did this change.


Technical information


In this section communication between Alfresco Repository and SOLR is revisited, in order to provide the right background to understand the discouraged and recommended deployment configurations.


Communication between Alfresco Repository and SOLR


The communication between Alfresco Repository and SOLR happens in both ways:

  • SOLR is polling Alfresco Repository for indexing (or tracking) the content, including changes in models, permissions, metadata and content. This polling is asynchronous and the frequency of these invocations can be scheduled using a cron expression named alfresco.cron in file. By default, this communication happens every 10 seconds.


SOLR Indexing scheduled task



Searching from an end user application

Simplified Deployment Diagrams

Despite Alfresco can be deployed in many different ways, a simplified scenario can be described in the following diagrams.


Three alternatives are analysed:

  • SOLR and Alfresco communicating with HTTP, directly exposing services from Tomcat and Jetty application servers (insecure)
  • SOLR and Alfresco communicating with HTTP, exposing services via a Web Proxy (NGINX or equivalent) to protect external accesses to private REST API services (secure)
  • SOLR and Alfresco communicating with Mutual Authentication TLS, directly exposing services from Tomcat and Jetty application servers (secure)


SOLR and Alfresco communicating with HTTP (insecure)

Unprotected deployment for ACS using http

When using HTTP, SOLR REST API is exposed without authentication requirements. This means that SOLR can perform tracking operations with Repository but also any external application can use SOLR REST API to get information from repository (metadata, permission, models and content) without authentication.


The access to the SOLR Web Console is also exposed, at it's available by default at http://localhost:8983/solr without authentication.


In this scenario, Repository information is exposed, so you must avoid using this configuration.

SOLR and Alfresco communicating with HTTP and services protected by Web Proxy (secure)


ACS deployment protected by Web Proxy


When using HTTP behind a Web Proxy (like NGINX), SOLR REST API is exposed without authentication requirements internally. This means that SOLR can perform tracking operations with Repository using internal HTTP port 8080, but the external access to this API is protected by the Web Proxy. Any external application trying to access to SOLR REST API without authentication, using the external HTTP port 80, will be blocked by the rules described before at "If you were using HTTP > Web Proxy" section.


This approach is the one provided for Docker Compose and Helm Charts by default, as they are using a pre-configured Web Proxy to expose internal services.


The access to the SOLR Web Console, available by default at http://localhost:8983/solr, can be also exposed by the Web Proxy to be served at http://localhost/solr including a simple credential protection or any other mechanism to avoid public access.


NGINX sample configuration is provided below.


# SOLR Web Console
location /solr/ {

proxy_pass http://solr6:8983/;

# Basic authentication
auth_basic "Solr web console";
auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;


In this scenario, Repository information is safely protected, so this configuration is recommended.


SOLR and Alfresco communicating with Mutual Authentication TLS (secure)

ACS deployment protected by Mutual Authentication TLS

When using Mutual Authentication TLS, SOLR REST API is exposed in SSL with clientAuth requirement. This means that SOLR can perform tracking operations with Repository using internal HTTPs port 8443 and signing the requests with the SOLR server certificate configured in Any external application trying to access to SOLR REST API using the HTTP port 8080, will be blocked by the Repository Web Filter. If the application uses the HTTPs port 8443, a clientAuth will be required and it will fail as the accepted server certificate is privately protected in SOLR server.


The access to the SOLR Web Console, available by default at https://localhost:8983/solr, is also protected by Mutual Authentication TLS. A client certificate, named browser.p12, is provided by the Alfresco SSL Generator in order to grant the access to this server.


In this scenario, Repository information is safely protected, so this configuration is recommended.


Search Codebase Restructure

Posted by tpage Employee Jun 21, 2019

You may have noticed some changes recently in the SearchServices codebase.  Before we go into the details it might be interesting to consider the number of git repositories that we were previously working with:

Git repositories used by the Search Team
This diagram just shows the projects we were editing for the latest version of Search Services.

If you find it hard to track the compatibility between different versions of ACS and Search Services[1] then you can imagine how we felt with all the different versions of these different internal libraries.


In January we started the process of consolidating this code by merging the solrclient history into Search Services (SEARCH-1395). The solrclient project was originally intended to support Solr 4 and Solr 6, however we subsequently took the decision not to support Search Services with Solr 4. Consequently the effort of maintaining a library with a separate lifecycle became redundant.

We made a similar change to our enterprise search codebase too, merging the git repositories for Insight Engine, Insight JDBC and Insight Zeppelin.


The next step is for us was to merge the enterprise and community code into a single repository.

A major motivation for this is that our end-to-end tests are currently in a separate project and we would prefer to store them in the same repository (on the same branch) as the production code. This will allow us to simultaneously make changes on feature branches to our production and test code. We have preemptively merged our test codebases, and have ticket for the rest of the work which we are hoping to start in the near future.


However we want to keep the Search Services code open source and so we've created a two-way community mirror that pushes all our community changes to GitHub and allows us to accept community pull requests and incorporate them with the enterprise code.


The current project structure now looks like this and, as mentioned, we're planning to simplify it further.


One side-effect of this is that we've introduced some new root directories (search-services and insight-engine) and a new root pom file. Our community repository does not contain the insight-engine code, and so needs to be built from within the search-services module. We have added this directory to our history too, as this will make it easier to copy bug fixes between branches and perform code comparisons. However this means that if you have an existing clone of the project then you will need to create a fresh clone.


If you come across any problems with the new codebase then please let us know (or send us a pull request!)  We're looking forward to spending more time adding value to the product and less time managing releases of internal libraries.


[1] Compatibility information for ACS and Search Services is available from our docs site.

Project Objective

(Link to the project in Github)

Note: the most up to date information about the project can be found in the github page (link above).


In this blog we cover the deployment of ACS (Alfresco Content Services) Enterprise 6.1 on Azure using VMs. The deployment consists of two mayor steps:

  1. Creating Azure Managed Images containing a base installation of the corresponding Alfresco component (Alfresco Content Services, Alfresco Search Services...).

  2. Building an Azure infrastructure (Load balancers, VMs, networks, public ips, etc) and deploying Alfresco Enterprise 6.1 to it.


The final ACS architecture looks like this:

Alfresco in Azure architecture



The tools and code used in this blog to deploy ACS Enterprise 6.1 are not officially supported by Alfresco. They are used as a POC to show you how you can use OpenSource tools to deploy and configure resources and applications to Microsoft Azure in an automated way.


The software used to build and deploy ACS Enterprise 6.1 is available in a public repository in GitHub.


Software requirements

The following software is required to create and deploy resources in Azure:

(Mac Os X versions)


  • Packer  1.4.0 - used to automate the creation of Azure Images.

  • Terraform 0.11.13-  used to create and update infrastructure resources.

  • Ansible 2.7.6 - Ansible is an IT automation tool used to deploy and configure systems.
  • OpenSSL 1.0.2f - used to decrypt credentials stored in a file.
  • Azure-cli 2.0.55 - used to create and check resources in Azure.


Make sure these tools have been installed on the computer you are using to build and deploy ACS Enterprise 6.1.


How to run the project


The project contains a bash script called The script will do the following:

  1. Create the managed Images in Azure.
  2. Build the infrastructure and deploy and configure Alfresco Enterprise 6.1.


However, before running the script, the Azure and Nexus credentials should be stored encrypted in a file so the script can use the credentials. 


Instructions about how to store the credentials and run the project are described below.


1. Authenticating to Azure


Azure-cli, Packer and Terraform need to authenticate to Azure in order to create resources. Since we are creating a large number of infrastructure resources, the user we authenticate with to Azure, needs to have administrator access.


In order to authenticate in Azure we need the following information:

  • Subscription ID
  • Tenant ID
  • Client ID
  • Client Secret


The following links provide information to get the above values:


Once you gathered all the credentials you should store them encrypted in a file so the values can be accessed by the script:

$ cat > ~/.az <<EOF
"client_id": "XXXXXX",
"client_secret": "XXXXXXXX",
"subscription_id": "XXXXXX",
"tenant_id": "XXXXXXXX"
$ openssl enc -aes-256-cbc -in ~/.az -out ~/.az.dat

$ chmod 400 ~/.az.dat
$ rm ~/.az


2. Authenticating to Nexus


Nexus is a repository manager used by Alfresco to publish software artifacts. Ansible will connect to Nexus repository to download the necessary software to install ACS Enterprise 6.1.
Alfresco Enterprise customers have Nexus credentials as part of their license subscription. Please refer to your CRM if you don't know or have your Nexus credentials.
In order to store your Nexus credentials in an encrypted file to use them in this project do the following:

$ echo "NEXUS_USER:NEXUS_PASSWORD" > ~/.nexus
$ openssl enc -aes-256-cbc -in ~/.nexus -out ~/.nexus.dat

$ chmod 400 ~/.nexus.dat
$ rm ~/.nexus


3. Set the correct values in run-azure script


There are some variables that should be configured before running the script:


  • NEXUS_FILE - location of the encrypted file containing the Nexus credentials. 
  • AZURE_FILE - location of the encrypted file containing the Azure credentials
  • DOMAIN_NAME_LABEL - domain name that will be used in the url to access the infrastructure. The format will be {DOMAIN_NAME_LABEL}-{UNIQ_ID}.{LOCATION} Example:

  • RESOURCE_GROUP_NAME_IMG - name of the Azure resource group where the managed images will be created.
  • RESOURCE_GROUP_NAME_DEPLOY - name of the Azure resource group where the infrastructure will be deployed.
  • LOCATION - name of the location used in Azure for the project. To get a list of locations use the following command:
az account list-locations

4. Download the Digital Workspace WAR file:


Download the "Digital workspace" war file from here and save it as packer-files/downloaded/digital-workspace.war (this file is not yet available in Nexus for an automated download).


5. Run the project script


Make sure you have execution permission in the script and then execute it:

$ ./


M A I N - M E N U



1. Run the entire project (create Azure Images, build infrastructure and deploy Alfresco Enterprise 6.1)

2. Create Azure Images

3. Build Azure infrastructure and deploy Alfresco Enterprise 6.1

4. Show infrastructure IPs and DNS names


Enter choice [ 1 - 4] 

Select option 1 if you want to run the entire project or option 2 followed by option 3 if you want to run it on stages. The last option will show the IPs and DNS names generated in case you need them again (the information is shown as well in options 1 and 3).


What happens when running the project


Creating managed Images in Azure


The first stage of the project consists in creating the following managed images:


  1. Alfresco Frontend Image - containing Alfresco Repository, Share, ADW (Alfresco Digital Workspace) and Apache Zeppelin.
  2. Insight Engine Image - containing Alfresco Search Services.
  3. Transform Service Image - containing the new Alfresco Transform Service.
  4. ActiveMQ Image - containing Apache ActiveMQ.
  5. DB Image - containing a base installation of MySQL Server.


The script calls packer to create every image individually. A CentOS 7.6 distribution image is used by packer as a baseline, and after uploading the required files (from packer-files folder), ansible is called for provisioning the image. In the provisioning phase, ansible downloads from Nexus and other repositories the necessary "artifacts" and installs them.


As the run-azure script runs you can see what is is doing during its execution...



Creating Images




Creating image AlfrescoFrontendImage



azure-arm output will be in this color.


==> azure-arm: Running builder ...

==> azure-arm: Getting tokens using client secret

    azure-arm: Creating Azure Resource Manager (ARM) client ...


==> azure-arm: Waiting for SSH to become available...

==> azure-arm: Connected to SSH!

==> azure-arm: Provisioning with shell script: /var/folders/z3/qv3400px0ys93js187p3fwwh0000gp/T/packer-shell046700839


==> azure-arm: Uploading packer-files/downloaded/digital-workspace.war => /var/tmp/ansible/downloaded/digital-workspace.war

digital-workspace.war 2.81 MiB / 2.81 MiB [===============================================================================================================================================] 100.00% 0s


==> azure-arm: Provisioning with Ansible...


And the time elapsed for every image created:


16 minutes and 37 seconds elapsed for AlfrescoFrontendImage image creation.


Building Azure infrastructure and deploying Alfresco


Once the images are created it's time to build the infrastructure using Terraform. The file contains the terraform configuration for the Azure and ACS deployments. Additionally contains some specific values (mostly private static ips) required in the deployment.


The output of the run-azure script when creating the infrastructure looks like this:


Deploying bastion



An execution plan has been generated and is shown below.

Resource actions are indicated with the following symbols:

  + create


Terraform will perform the following actions:


  + azurerm_network_interface.bastionnic

      id:                                                                    <computed>

      applied_dns_servers.#:                                                 <computed>

      dns_servers.#:                                                         <computed>

      enable_accelerated_networking:                                         "false"

      enable_ip_forwarding:                                                  "false"

      internal_dns_name_label:                                               <computed>

      internal_fqdn:                                                         <computed>



azurerm_virtual_machine.bastionvm: Provisioning with 'ansible'...

azurerm_virtual_machine.bastionvm (ansible): Connecting to remote host via SSH...

azurerm_virtual_machine.bastionvm (ansible):   Host: 51.XXX.XXX.10

azurerm_virtual_machine.bastionvm (ansible):   User: azureuser

azurerm_virtual_machine.bastionvm (ansible):   Password: false

azurerm_virtual_machine.bastionvm (ansible):   Private key: true

azurerm_virtual_machine.bastionvm (ansible):   SSH Agent: true

azurerm_virtual_machine.bastionvm (ansible):   Checking Host Key: false

azurerm_virtual_machine.bastionvm (ansible): Connected!


And the time elapsed for deploying the infrastructure:


17 minutes and 5 seconds elapsed for entire infrastructure deployment.


Connecting to the infrastructure 


This is the screen shown when the infrastructure has been created (or when selecting the option 4 in the menu):






VM created on resource group "Alfresco_deployment"


Alfresco Content Services 6.1 (share) ----------->

Alfresco Content Services 6.1 (admin console) --->

Digital Workspace ------------------------------->

Alfresco Insight Engine 1.0.0 ------------------->


Via Bastion urls:


Alfresco Search Services 1.2.0 ------------------> http://51.XX.XX.81:8983




Application gateway IP: 52.XX.XX.250

Bastion ip (ssh): 51.XX.XX.81


Through the bastion node it's possible to connect to the VMs inside the private network. As follows there's a table containing all the endpoints reachable from the bastion IP:


EndpointNAT PortReal portVM
ActiveMQ console81618161activemq-0
ActiveMQ console81628161activemq-1
Solr admin console89838983insight-0
Solr admin console89848983insight-1
Libreoffice transformer80908090transformation-0



Imagemagick transformer80928092transformation-0
Tika transformer80938093transformation-0
Shared File Store (console)80948094transformation-0
Transform router80958095transformation-0
Libreoffice transformer81908090transformation-1



Imagemagick transformer81928092transformation-1
Tika transformer81938093transformation-1
Shared File Store (console)81948094transformation-1
Transform router81958095transformation-1


To do list


  • Secure the infrastructure using https and fine tune other security aspects.
  • Capability to integrate custom amps when creating the infrastructure.
  • Use the customer Alfresco license when deploying Alfresco. (Currently the license installed is a trial license of 2 days however a new license can be uploaded through the admin console).
  • Use autoscaling for the VMs
  • Use Azure SQL DB instead of a dedicated VM with MySQL.
  • Many other things ;-)



Project Objective

In this blog we cover the deployment of ACS (Alfresco Content Services) 6.1 Enterprise on AWS. The deployment consists of two mayor steps:

  1. Building AWS AMIs (Amazon Machine Images) containing a base installation of ACS 6.1 Enterprise.
  2. Building AWS infrastructure (VPC, ELB, RDS, etc) and deploying ACS 6.1 Enterprise to it.


Please make sure your Alfresco license subscription entitles you to install and run ACS 6.1 Enterprise and Alfresco Search Services with Insight Engine.


The final ACS architecture looks like this:



The tools and code used in this blog to deploy ACS 6.1Enterprise are not officially supported by Alfresco. They are used as a POC to show you how you can use OpenSource tools to deploy and configure resources and applications to AWS in an automated way.


The software used to build and deploy ACS 6.1 Enterprise is available in a public repository in GitHub.


Software requirements

The following software is required to create and deploy resources in AWS:

  • Packer - used to automate the creation of AMIs.
  • Terraform - used to create and update infrastructure resources.
  •  Ansible - Ansible is an IT automation tool used to deploy and configure systems.
  • AWS CLI - The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services.


Make sure these tools have been installed on the computer you are using to build and deploy ACS 6.1Enterprise.


Authenticating to AWS

Both Packer and Terraform need to authenticate to AWS in order to create resources. Since we are creating a large number of infrastructure resources, the user we authenticate with to AWS, needs to have administrator access.

There are multiple ways to configure AWS credentials for Packer and Terraform, the following links will show you how to do it:


Note: Whatever method you use make sure you keep your AWS credentials private and secure at all times.

Authenticating to Nexus

Nexus is a repository manager used by Alfresco to publish software artifacts. Packer will connect to Nexus repository to download the necessary software to install ACS 6.1 Enterprise.

Alfresco Enterprise customers have Nexus credentials as part of their license subscription. Please refer to your CRM if you don't know or have your Nexus credentials.


Building AWS AMIs

The first step to deploy ACS 6.1 Enterprise is to build two types of AMIs:

  1. Repository AMI - containing Alfresco Repository, Share and ADW (Alfresco Digital Workspace)
  2. Search Services AMI - containing Alfresco Repository and Search Services with Insight Engine.


Repository AMI

For this process we use Packer and Ansible. We first export the "nexus_user" and "nexus_password" environment variables containing credentials to access the Nexus repository. These are stored in the ~/.nexus-cfg file contains the following.

export nexus_user=xxxxxxxxx
export nexus_password=xxxxxxxxx


Note that the .nexus-cfg file is in the user home folder, keep this file and its contents private and secured at all times.


If you want to include custom amps add them to the amps and amps_share folder and they will be deployed to the AMI.

For custom jar files add them to the modules/platform and modules/share folders.


If you want to deploy ADW (Alfresco Digital Workspace) place the digital-workspace.war file in the acs-61-files/downloaded folder.


We can now execute packer by calling the script.

cd acs-61-repo-aws-packer

This shell script will load the nexus environment variables and call packer build using a template file for the provisioning of the AMI and a variables file containing deployment specific information such as your default VPC Id, the AWS region, etc.

Make sure you change the value of the vpc_id variable to use your default VPC Id.


Search Services AMI

As on the previous section, we use Packer and Ansible to create a Search Services AMI.


Make sure you change the value of the vpc_id variable to your default VPC Id before running the script.

cd acs-61-repo-aws-packer


As the script runs you can see what is is doing during its execution...

▶ ./
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: acs-61-repo-1557828971
amazon-ebs: Found Image ID: ami-00846a67
==> amazon-ebs: Creating temporary keypair: packer_5cda956b-bd62-1d09-cef2-639152741025
==> amazon-ebs: Creating temporary security group for this instance: packer_5cda956b-345b-2321-afd5-40b1b06a6bc1
==> amazon-ebs: Authorizing access to port 22 from in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
amazon-ebs: Adding tag: "Name": "Packer Builder"
amazon-ebs: Instance ID: i-0f80505eb56dccbb7
==> amazon-ebs: Waiting for instance (i-0f80505eb56dccbb7) to become ready...


On completion the script will output the AMI id of the newly created AMI. Keep track of both AMI Ids, as we will need to use them in the Terraform script next.

Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
eu-west-2: ami-08fd6196500dbcb01


Building the AWS Infrastructure and Deploying ACS 6.1 Enterprise

Now that we have created both the Repository and the Search Services AMIs we can start building the AWS infrastructure and deploy ACS 6.1 Enterprise.

In the acs-61-aws-terraform folder we have the terraform.tfvars containing configuration specific for the AWS and ACS deployments.


Some of the variables that will need to be updated are:

  • resource-prefix - this is used to name all resources created with some initials to identify the resources belonging to this deployment.
  • aws-region
  • aws-availability-zones
  • vpc-cidr
  • autoscaling-group-key-name
  • s3-bucket-location


and of course we need to set the auto scaling image id with the newly generated AMIs

  • autoscaling-repo-group-image-id
  • autoscaling-solr-group-image-id


Once the configuration has been set we are ready to start building the solution. We first need initialize terraform with the "terraform init" command:


▶ terraform init
Initializing modules...
- module.vpc
- module.rds
- module.alfresco-repo
- module.alfresco-solr
- module.bastion
- module.alb
- module.internal-nlb
- module.activemq

Initializing provider plugins...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* version = "~> 2.10"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.


We can now issue the apply command to start the build. Upon completion (it will take around 15 minutes) we will get notification of the URLs available to connect to Alfresco.

Apply complete! Resources: 51 added, 0 changed, 0 destroyed.


Alfresco Digital Workspace =
Alfresco Share =
Alfresco Solr =
Alfresco Zeppelin =
RDS Endpoint =
VPC ID = vpc-006f0c6354656e96d5c


To destroy the resources issue a "terraform destroy" command.


Terraform will perform the following actions:

Plan: 0 to add, 0 to change, 51 to destroy.

Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.


To do list

There are a couple of things to add to this project:

  • CI/CD scripts - I have already implemented this and will publish it on a different blog.
  • On the Search Services instances we should download a backup of the Solr indexes when starting a new instance instead of building the indexes from scratch.

Recently I have been involved in an investigation task about the Alfresco Tracker Subsystem, with a specific focus on the Content Tracker. This post is the output of the analysis, which can also be found in the SearchServices repository, under the documentation folder.   

The ContentTracker is part of the Alfresco Tracker Subsystem which is composed by the following members:

  • ModelTracker: for listening on model changes
  • ContentTracker: described in this post
  • MetadataTracker: for tracking changes in metadata nodes
  • AclTracker: for listening on ACLs changes
  • CascadeTracker: which manages cascade updates (i.e. updates related with the nodes hierarchy)
  • CommitTracker: which provides commit and rollback capabilities

Each Solr that composes your search infrastructure (regardless it holds a monolithic core or a shard) has a singleton instance of each tracker type, which is registered, configured and scheduled at SolrCore startup. The SolrCore holds a TrackerRegistry for maintaining a list of all "active" tracker instances.

The ContentTracker queries for documents with "unclean" content (i.e. data whose content has been modified in Alfresco), and then updates them. Periodically, at a configurable frequency, the ContentTracker checks for transactions containing nodes that have been marked as "Dirty" (changed) or "New". Then,

  • it retrieves the cached version of that data from the ContentStore
  • it retrieves the corresponding (text) content from Alfresco
  • it updates the ContentStore
  • it re-indexes the data in the hosting Solr instance


Later, the CommitTracker will persist those changes. 


The Tracking Subsystem

The class diagram below provides a high-level overview about the main classes involved in the Tracker Subsystem.

As you can see from the diagram, there's an abstract interface definition (Tracker) which declares what is expected by a Tracker and a Layer Supertype (AbstractTracker) which adopts a TemplateMethod [1] approach. It provides the common behavior and features inherited by all trackers, mainly in terms of:

  • Configuration
  • State definition (e.g. isSlave, isMaster, isInRollbackMode, isShutdown)
  • Constraints (e.g. there must be only one running instance of a given tracker type in a given moment)
  • Locking: the two Semaphore instances depicted in the diagram used for a) implementing the constraint described in the previous point b) providing an inter-trackers synchronisation mechanism.

The Tracker behavior is defined in the track() method that each tracker must implement. As said above, the AbstractTracker forces a common behaviour on all trackers by declaring a final version of that method, and then it delegates to the concrete trackers (subclasses) the specific logic by requiring them the implementation of the doTrack() method.

Each tracker is a stateful object which is initialized, registered in a TrackerRegistry and scheduled at startup in the SolrCoreLoadRegistration class. The other relevant classes depicted in the diagram are:

  • SolrCore: the dashed dependency relationship means that a Tracker doesn't hold a stable reference to the SolrCore: it obtains that reference each time it's needed.
  • ThreadHandler: The ThreadExecutionPool manager which holds a pool of threads needed for scheduling asynchronous tasks (i.e. unclean content reindexing)
  • TrackerState: being a shared instance across all trackers, it would have been called something like TrackersState or TrackerSubsystemState. It is used for holding the trackers state (e.g. lastTxIdOnServer, trackerCycles, lastStartTime)
  • TrackerStats: maintains a global stats about all trackers. Following the same approach of the TrackerState, it is a shared instance and therefore the name is a little bit misleading because it is related to all trackers
  • SOLRAPIClient: this is the HTTP proxy / facade towards Alfresco REST API: in the sequence diagrams these interactions are depicted in green
  • SolrInformationServer: The Solr binding for the InformationServer interface, which defines the abstract contract of the underlying search infrastructure

Startup and Shutdown

The Trackers startup and registration flow is depicted in the following sequence diagram:

Solr provides, through the interface SolrEventListener, a notification mechanism for registering custom plugins during a SolrCorelifecycle. The Tracker Subsystem is initialized, configured and scheduled in the SolrCoreLoadListener which delegates the concrete work to SolrCoreLoadRegistration. Here, a new instance of each tracker is created, configured, registered and then scheduled by means of a Quartz Scheduler. Trackers can share a common frequency (as defined in the alfresco.cronproperty) or they can have a specific configuration (e.g. alfresco.content.tracker.cron).

The SolrCoreLoadRegistration also registers a shutdown hook which makes sure all registered trackers will follow the same hosting SolrCore lifecycle.

Content Tracking

The sequence diagram below details what happens in a single tracking task executed by the ContentTracker: 



At a given frequency (which again, can be the same for each tracker or overriden per tracker type) the Quartz Scheduler invokes the doTrack() method of the ContentTracker. Prior to that, the logic in the AbstractTracker is executed following the TemplateMethod [1] described above; specifically the "Running" lock is acquired and the tracker is put in a "Running" state.


Then the ContentTracker does the following:

  • get documents with "unclean" content
  • if that list is not empty, each document is scheduled (asynchronously) for being updated, in the content store and in the index


In order to do that, the ContentTracker never uses directly the proxy towards ACS (i.e. the SOLRAPIClient instance); instead, it delegates that logic to the SolrInformationServer class. The first step (getDocsWithUncleanContent) searches in the local index all transactions which are associated to documents that have been marked as "Dirty" or "New". The field where this information is recorded is FTSSTATUS; it could have one of the following values:


  • Dirty: content has been updated / changed
  • New: content is new
  • Clean: content is up to date, there's no need to refresh it


The "Dirty" documents are returned as triples containing the tenant, the ACL identifier and the DB identifier.


NOTE: this first phase uses only the local Solr index, no remote call is involved.


If the list of Tenant/ACLID/DBID triples is not empty, that means we need to fetch and update the text content of the corresponding documents. In order to do that, each document is wrapped in a Runnable object and submitted to a thread pool executor. That makes each document content processing asynchronous.


The ContentIndexWorkerRunnable, once executed, delegates the actual update to the SolrInformationServer which, as said above, contains the logic needed for dealing with the underlying Solr infrastructure; specifically:


  • the document that needs to be refreshed, uniquely identified by the tenant and the db identifier, is retrieved from the local content store. In case the cached document cannot be found in the content store, the /api/solr/metadata remote API is contacted in order to rebuild the document (only metadata) from scratch.
  • the api/solr/textContent is called in order to fetch the text content associated with the node, plus the transformation metadata (e.g, status, exception, elapsed time)
  • if the alfresco.fingerprint configuration property is set to true and the retrieved text is not empty the fingerprint is computed and stored in the MINHASH field of the document
  • the content fields are set
  • the document is marked as clean (i.e. FTSSTATUS = "Clean") since its content is now up to date
  • the cached version is overwritten in the content store with the up to date definition
  • the document (which is a SolrInputDocument instance) is indexed in Solr



The Rollback sequence diagram illustrates how the rollback process works:

The commit/rollback process is a responsibility of the CommitTracker, so the ContentTracker is involved in these processes only indirectly.

When it is executed, the CommitTracker acquires the execution locks from the MetadataTracker and the AclTracker. Then it checks if one of them is in a rollback state. As we can imagine, that check will return true if some unhandled exception has occurred during indexing.

If one of the two trackers above reports an active rollback state, the CommitTracker lists all trackers, invalidates their state and issues a rollback command to Solr. That means any update sent to Solr by any tracker will be reverted.

How does the ContentTracker work in shard mode?

The only source that the ContentTracker checks in order to determine the "unclean" content that needs to be updated is the local index. As consequence of that, the ContentTracker behavior is the same regardless the search infrastructure shape and the context where the hosting Solr instance lives. That is, if we are running a standalone Solr instance there will be one a ContentTracker for each core watching the corresponding (monolithic) index. If instead we are in a sharded scenario, each shard will have a ContentTracker instance that will use the local shard index.


How does the ContentTracker work in Master/Slave mode?

In order to properly work in a Master/Slave infrastructure, the Tracker Subsystem (not the only ContentTracker) needs to be

  • enabled on Master(s)
  • disabled on Slaves

The only exceptions to that rule are about:

  • The MetadataTracker: only if the search infrastructure uses dynamic sharding [2] the Metadata tracker is in charge to register the Solr instance (the Shard) to Alfresco so it will be included in the subsequent queries. The tracker itself, in this scenario, won't track anything.
  • The ModelTracker: each Solr instance pulls, by means of this tracker, the custom models from Alfresco, so it must be enabled in any case.

The document file in the SearchService repository provides an additional paragraph with the configuration attributes related with the Tracker subsystem. I didn't put that long table in this post because it doesn't add any information: if you need to configure the trackers just have a look at the end of that document.     


What's next?

The Tracker Subsystem is one of the main areas where the Search Team is devolving analysis and investigation efforts: that will allow to find a space for introducing further improvements in the architecture. 





I'm pleased to announce the availability of SDK 4.0 and 3.1.


The artifacts have been released to Maven Central. It might be a few days until the global archetype catalog updates, but read on for more details on how to get started today!


This release will enable developers to extend and customize Alfresco Content Services 6.0 and 6.1. In addition, the SDK 3.1 release brings a number of bug fixes as well as a few highly requested features.


Please note

SDK 4.0 is only compatible with ACS 6.0 and 6.1.

If you wish to extend and customize ACS 5.2 or older, please use SDK 3.1.


Building on top of the first beta of 4.0  that we released in January, we received a lot of feedback and we appreciate all the involvement, contributions and feedback.


SDK 4.0 brings changes to the underlying logic for running and testing your customizations. This means that the SDK now leverage containers to launch the full suite of Alfresco Content Services. By leveraging Docker containers we make it easy to start additional services, such as Transform Service, Search Services and much more.


If you are already familiar with SDK 3.X you will feel right at home. We still offer the All-in-one archetype, as well as platform-jar and share-jar archetypes. The project structure changed slightly to accommodate for the Docker setup, and the pom.xml has changed.


Key changes and improvements:

  • Two new sub modules introduced for All-in-one: platform-docker and share-docker. These modules provides the required setup in order to extend the ACS Docker containers with your customizations
  • A new Docker folder contains a docker-compose.yml file. This file is filtered by Maven to easily enable switching between versions
  • SDK 4 no longer depends on the Alfresco Maven Plugin since all runner logic is moved to Docker
  • Full support for ACS 6.0 and 6.1, both Enterprise and Community
  • Submodules are now renamed to be prefixed with the project
  • Full support for JRebel for hot reloading
  • More commands via / run.bat


Getting Started

We have a new getting started guide in the documentation, if you just can't wait to get your hands into the code here are a few things you need:

  1. Java 11
  2. Maven 3.3+ (we always recommend using the latest version)
  3. Docker


Note: If you are on Windows 7, you will need to install Docker Toolbox.


Once you have all three installed you can create an All-in-one with the following command:

mvn archetype:generate -DarchetypeGroupId=org.alfresco.maven.archetype -DarchetypeArtifactId=alfresco-allinone-archetype -DarchetypeVersion=4.0.0


In a few days once the global archetype catalog gets updated you'll be able to use the same old command: mvn

archetype:generate -Dfilter=org.alfresco:


Please have a look at the documentation and feel free to ask questions or raise an issue (or even better, a pull request!)


SDK 3.1

In addition to releasing SDK 4.0, we have also issued a minor release of the 3.x branch.


You can view the full list of closed issues here, but the main highlight here is that there is now a new goal in the Alfresco Maven Plugin which will only build the WAR files with your customizations applied.


Furthermore, SDK 3.1 now allows you to control the order AMPs are applied to the WAR file. When you define your platformModules and shareModules in your pom, the last defined will be the last to be applied.


If you have existing SDK 3.0 projects you can easily upgrade by simple changing the <alfresco.sdk.version /> property in your pom.


I want to thank everyone who contributed, we received a record number of pull requests from our wonderful community. We really appreciate it!

Being able to edit a file concurrently by multiple users is a need we’re coming across more and more when dicussing with customers on the field.
At the time of writing, the out-of-the-box solution to deliver this kind of feature within Alfresco is to use the GoogleDocs module.
This module allows for content stored in Alfresco to be collaboratively edited using Google’s online application suite (text editor, spreasheet editor, presentations) and saved back in the repository.
However some customer may not want to use Google services for different reasons (e.g, cost or data sensitivity), in which case there are far less options.
If you’re concerned about your data being sent to a public cloud and prefer having them securely stored on-premise in Alfresco instead, there may be a solution to help you.

LibreOffice OnLine (LOOL)

Alfresco uses LibreOffice (and formerly OpenOffice) for a very long time. This is one of the component providing our out-of-the-box transformation service (either through OODirect or jodconverter).
After delivering an opensource productivity Suite on the desktop, the Libreoffice team has started working on a similar feature set with a more SaaS approach: LibreOffice OnLine (LOOL).
Don’t get me wrong here: LOOL is not a SaaS solution you have to subscribe to, and that’s the intersting thing with it. LOOL is a service you can install on-premise in order to provide edition tools for office documents. And of course, as that’s what’s our main interest here, it provide collaborative edition capabilities. LOOL itself already provides a solution to having collaborative edition, while keeping your data in compliance with your SSI company policy!
Here I’ll detail how to integrate LOOL with your prefered on-premise content management system, thus bringing the collaborative edition feature inside Alfresco!

Alfresco integration

LibreOffice OnLine is actually a WOPI client and needs to talk to a WOPI server.

If you want to know more about the WOPi protocol you can check its definition here

The WOPI server role will be endorsed by Alfresco itself using 2 modules (one for the repo and one for Share). Those AMPs have been created by Magenta. All credits goes to them, here, I’m just giving guidance on how to install and configure it for Alfresco Content Services:

In terms of network flows, the following diagram shows what conenction are used

network flows Alfresco Libreofice Online

In this document we’ll use Alfresco Content Service 5.2.4.


LOOL installation

Fortunately it is now very simple to install LOOL (using the CODE distribution). The simple commands bellow should work for a Debian based Linux distribution.
Alongside this document we’ll use Debian 9.

$ echo 'deb ./' | sudo tee /etc/apt/sources.list.d/code.list
$ sudo apt-key adv --keyserver --recv-keys 0C54D189F4BA284D
$ sudo apt update
$ sudo apt install loolwsd code-brand

If you’re just testing you’ll probably be interested in using the docker image available at docker hub

LOOL configuration

By default the office online suite is configured to use SSL but the certificates are not provided. We then have to create those (or disable SSL if not targeting production).

$ sudo mkdir /etc/loolwsd/ssl

Copy to this newly created folder:

  • the certificate private key: /etc/loolwsd/ssl/loolwsd.key (make sure it’s only readable to the user runnning LOOL: lool)
  • the certificate itself: /etc/loolwsd/ssl/loolwsd.crt
  • the public CA certificate: /etc/loolwsd/ssl/cacert.pem

If you want ot use selfsigned certificates, this is a bit more tricky but do-able. Start with the commands bellow to generate the self-signed certificate:

$ sudo openssl genrsa -out /etc/loolwsd/ssl/loolwsd.key
$ sudo chown lool /etc/loolwsd/ssl/loolwsd.key
$ sudo chmod 400 /etc/loolwsd/ssl/loolwsd.key
$ cp /etc/ssl/openssl.cnf /tmp/loolwsd_ssl.cnf
$ cat >> /tmp/loolwsd_ssl.cnf <<EOT
[ san ]
subjectAltName = @alt_names
IP.1 =
$ sudo openssl req -new -x509 -sha256 -nodes -key /etc/loolwsd/ssl/loolwsd.key -days 9999 -out /etc/loolwsd/ssl/loolwsd.crt -config /tmp/loolwsd_ssl.cnf -extensions san

IP.1 must match the IP address where the libreoffice online service is available. Change it to match your needs

Additionnally, if you’re using self signed certificate, it is required the Alfresco JVM trusts this certificate .

$ keytool -importcert -alias lool -file /etc/loolwsd/ssl/loolwsd.crt -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -storetype JKS

Of course, keystore path, type and passwords must match your environment

The client browser must also trust this certificate!

If LOOL traffic is wrapped in SSL, you’ll also need to have Alfresco protected by SSL. This is because most browser today will prevent pages with mixed content (http & https) from being displayed.
It means you have to configure Alfresco for SSL. Please refer to the official documentation in order to do that:
Again if you use a selfsigned certificate (or a certificate from a private pki ) for Alfresco, it is required to let LOOL trust that certificate. The way to do it depends on the distribution the service is running on. On Debian-like systems you can do:

$ keytool -exportcert -alias -keystore alf_data/keystore/ssl.keystore -storetype JCEKS -storepass kT9X6oe68t | openssl x509 -inform DER -outform PEM -in - | sudo tee /usr/local/ca-certificates/alfresco.crt
$ sudo update-ca-certificates

The example command above uses the default Alfresco Keystore, path and password which you should have changed. Make sure to use the correct ones for your environment.

Now open the service configuration file /etc/loolwsd/loolwsd.xml and edit the ssl section as shown bellow:

<ssl desc="SSL settings">
 <enable type="bool" desc="Controls whether SSL encryption is enable (do not disable for production deployment). If default is false, must first be compiled with SSL support to enable." default="true">true</enable>
 <termination desc="Connection via proxy where loolwsd acts as working via https, but actually uses http." type="bool" default="true">false</termination>
 <cert_file_path desc="Path to the cert file" relative="false">/etc/loolwsd/ssl/loolwsd.crt</cert_file_path>
 <key_file_path desc="Path to the key file" relative="false">/etc/loolwsd/ssl/loolwsd.key</key_file_path>
 <ca_file_path desc="Path to the ca file" relative="false">/etc/loolwsd/ca-chain.cert.pem</ca_file_path>
 <cipher_list desc="List of OpenSSL ciphers to accept" default="ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"></cipher_list>
 <hpkp desc="Enable HTTP Public key pinning" enable="false" report_only="false">
  <max_age desc="HPKP's max-age directive - time in seconds browser should remember the pins" enable="true">1000</max_age>
<report_uri desc="HPKP's report-uri directive - pin validation failure are reported at this URL" enable="false"></report_uri>
 <pins desc="Base64 encoded SPKI fingerprints of keys to be pinned">

Also edit the net section to match your needs:

<net desc="Network settings">
 <proto type="string" default="all" desc="Protocol to use IPv4, IPv6 or all for both">all</proto>
 <listen type="string" default="any" desc="Listen address that loolwsd binds to. Can be 'any' or 'loopback'.">any</listen>
 <service_root type="path" default="" desc="Prefix all the pages, websockets, etc. with this path."></service_root>
 <post_allow desc="Allow/deny client IP address for POST(REST)." allow="true">
 <host desc="The IPv4 private 192.168 block as plain IPv4 dotted decimal addresses.">192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
 <host desc="Ditto, but as IPv4-mapped IPv6 addresses">::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3}</host>
 <host desc="The IPv4 loopback (localhost) address.">127\.0\.0\.1</host>
 <host desc="Ditto, but as IPv4-mapped IPv6 address">::ffff:127\.0\.0\.1</host>
 <host desc="The IPv6 loopback (localhost) address.">::1</host>
 <frame_ancestors desc="Specify who is allowed to embed the LO Online iframe (loolwsd and WOPI host are always allowed). Separate multiple hosts by space."></frame_ancestors>

Pay attention to the post_allow element, its value has to match the IP of your clients (all the browser which may request to edit files). The default configuration is to allow local access and access from a network.

The frame_ancestors element should be left alone as the wopi host (alfresco) is normally part of it by default. However there may be cases where it is required (e.g if running on a non-default network port)

Now make sure to have you alfresco host declared as an authorized wopi host in the storage section:

<storage desc="Backend storage">
    <filesystem allow="false" />
    <wopi desc="Allow/deny wopi storage. Mutually exclusive with webdav." allow="true">
       <host desc="Allow from Alfresco" allow="true"></host>
        <max_file_size desc="Maximum document size in bytes to load. 0 for unlimited." type="uint">0</max_file_size>                                                                      
    <webdav desc="Allow/deny webdav storage. Mutually exclusive with wopi." allow="false">
        <host desc="Hostname to allow" allow="false">localhost</host>

the wopi host specified must match you alfresco hostname as used by web browsers

On the Alfresco side add the following properties to the file:


Where loolhost is the server name where you installed LOOL, and alfrescohost the local server where alfresco is running
It is possible to install both on the same server of course.

Only use FQDN names (matching the certificates CN if using SSL), do not use localhost.

Use appropriate ports

Applying AMPs

There are 2 AMPs available. In order to turn Alfresco into a WOPI host you’ll need the repo AMP, and to add the necessary Share pages and buttons to allow UI integration the share AMP is needed.
We’ll first need to get the sources and build them:

$ git clone
$ cd alfresco-repo-libreoffice-online-module
$ vim pom.xml
$ mvn package

When editing the pom.xml make sure to:

  • set alfresco.platform.version & alfresco.share.version to your version in the repo pom.xml
  • set to the surf version matching you alfresco version in the share pom.xml
  • set maven.alfresco.edition to enterprise

Copy the resulting .amp files located in target/ to the amps and amps_share folders of your alfresco installation and run:

$ ./bin/

You can now restart the services:

$ sudo systemctl restart loolwsd
$ ./ restart tomcat

You can now test editing Office documents simultaneously with differents users and see how convenient LibreOffice OnLine makes it.

Below examples of spreadsheet and presentations concurrent edition by "Administrator" and "Alex" users:


Each user can see what the others are doing and who's editing.


calc collab

impress collabAs you can see in the screenshot above, the share module needs some tweaking if you're not using english locale. But that should really just be a matter of adding the right message bundle. to the share AMP

In this blog post I would like to share the upcoming changes to the contribution process for Alfresco components. In short, the well known ALF project ( in our Jira is going to be archived.

For quite a while the ALF project was serving as a home of all the contributions from Community members, accommodating the feature requests, bug descriptions and patches. The world around it has changed and the project is no longer the easiest way for us to listen to Community and for Community to talk to us. We would like to propose to use GitHub as the place for collaboration, to be as close to the code as possible.

This is how it can look like.

The process will include a smaller number of steps to make the contribution easier to submit and approve:


How to find the right repository?


Alfresco Content Services has a lot of components, so it might be quite hard to find the right one to submit your issue. The following list contains all of the relevant repositories and short description of each of them.

If you are unsure which one to select if you experience an issue in Alfresco application, you can use one of the packaging projects to raise the issue. If you know exactly where the issue occurs, please create an issue in the relevant project instead of packaging projects. Needless to say that it is a lot easier for us to review and approve the PR with exact changes than reacting to an issue description without any points to code. Although we will try our best to pay attention to all of them.

Please keep in mind that this list may change over time.

If the feedback will be positive, we will gradually make the following changes:

Here's a brief overview of the new Alfresco Transform Service that is being introduced as an option for Alfresco Content Services (ACS) 6.1. It also includes evolutionary changes to the ACS Repository.

For those that missed DevCon 2019 you can also refer to following slides Transforming Transformers in ACS 6.1 and beyond.
Transforming Transformersfor ACS 6.1 and beyond !Jan VonkaArchitect & Product Owner@ Alfresco
The video can be found here:
I will try to come back and update this blog post with links to other related slides & videos such as:
If you've been closely following the ACS reference deployment projects, you may already have noticed the new Transform Service. This is available now alongside the ACS 6.1 release.
Helm Deployment Components
The Transform Service is an optional component that is enabled by default in the ACS reference deployments, whether using Docker Compose, Helm / Kubernetes or the AWS EKS stack.
Alternatively, you can access via ACS download trial.
Overview of the Transform Service
The following overview is extracted from the documentation. Thanks Adelaide Nxumalo.
The Transform Service handles the essential transforms, such as Microsoft Office documents, images, and PDFs. These include PNG for thumbnails, PDF and JPEG for downloads and previews.
The following diagram shows a simple representation of the Transform Service components:

This shows an example implementation of how you can deploy into AWS, using a number of managed services:

  • Amazon EKS - Elastic Container Service for Kubernetes
  • Amazon MQ - Managed message broker service for Apache ActiveMQ
  • Amazon EFS - Amazon Elastic File System
You can replace the AWS services (EKS, MQ, and EFS) with a self-managed Kubernetes cluster, ActiveMQ (configured with failover), and a shared file store, such as NFS.

The main components of the Transform Service are:

  • Content Repository (ACS): This is the repository where documents and other content resides. The repository produces and consumes events destined for the message broker (such as ActiveMQ or Amazon MQ). It also reads and writes documents to the shared file store.
  • ActiveMQ: This is the message broker (either a self-managed ActiveMQ instance or Amazon MQ), where the repository and the Transform Router send image transform requests and responses. These JSON-based messages are then passed to the Transform Router.
  • Transform Router: The Transform Router allows simple (single-step) and pipeline (multi-step) transforms that are passed to the Transform Engines. The Transform Router (and The Transform Engines) run as independently scalable Docker containers.
  • Transform Engines: The Transform Engines transform files sent by the repository. Here are some example transformation for each Transform Engine (this is not an exhaustive list):
    • LibreOffice (e.g. docx to pdf)
    • ImageMagick (e.g. resize)
    • Alfresco PDF Renderer (e.g. pdf to png)
    • Tika (e.g. docx to plain text)
  • Shared File Store: This is used as temporary storage for the original source file (stored by the repository), intermediate files (or multi-step transforms), and the final transformed target file. The target file is retrieved by the repository after it's been processed by one or more of the Transform Engines.

For more details, please refer to the Transform Service documentation:



Please try out try out ACS 6.1 reference deployments with the new Transform Service.


We look forward to your feedback. Please free to contact Preeti Nirwal or myself.




Jan Vonka & the INT team (Integrations n Transformations)




Alfresco 6.0 switching to container deployment is a good thing, but Docker has efficiency issues on Mac. What's wrong with a well configured stand-alone Alfresco Community 6.0? Nothing!


Download Three Files

The attached Bash script automates the installation of Alfresco Community 6.0. It looks for 3 downloaded files in the same folder of the script or a folder specified as the script's first parameter.

The above links are for Tomcat 8.5.37, ACS 6.0.7-ga, Search Services 1.3.0.


Run The Install Script

Create an empty folder into which a stand-alone Alfresco Community will be installed. Be sure to set the environment variable JAVA_HOME. Then run the installation script, assuming everything else is in the parent folder:

mkdir 6.0.7-ga
cd 6.0.7-ga

The script will install all three archives, create or modify configuration files and startup scripts, download MySQL JDBC driver and the missing PDF renderer for Mac, start Solr6. It also applies the Share module for alfresco.war, which will ask you to press a few keys in between.


Create Your MySQL Schema

The script creates file tomcat/shared/classes/ with these MySQL database settings:

  • Schema name:  alf607ce
  • User name:  alfresco
  • Password:  alfresco

Here are the MySQL commands to create the schema:

$ mysql -u root
create schema alf607ce default character set utf8;
grant all on alf607ce.* to 'alfresco'@'localhost' identified by 'alfresco' with grant option;


Start and Stop Alfresco

./ start
./ jpda start # Start with debug
./ stop


Start and Stop Solr 6

The install script already starts Solr 6 for you. Subsequent start and stop commands are:

search-services/solr/bin/solr start
search-services/solr/bin/solr stop


Other Versions of Alfresco

This install script also works for 6.1.1. There is a bug in 6.1.2-ga that prevents alfresco.war from fully deployed and run in Tomcat. Just don't use it.


Filter Blog

By date: By tag: