This guide describes how to develop extensions and customize Alfresco Process Services.
Before beginning, you should read the Administering [1] section to make sure you have an understanding of how Alfresco Process Services is installed and configured.
To learn more about Alfresco Process Services architecture, see our Alfresco ArchiTech Talks video [2].
For more information about Activiti BPM, see Activiti.org [18].
The following diagram gives a high-level overview of the technical components in Alfresco Process Services.
Alfresco Process Services is packaged as a standard Java Web application (WAR file) that can be deployed in any supported Java web container. The WAR file contains the Java logic, REST API resources, and the user interface HTML and JavaScript files. The application is stateless, which means it does not use any sessions, and requests can be handled by any node in a clustered setup (see Cluster configuration and monitoring [19] for more information on multi-node setup).
Additional technical implementation details:
The Process Engine (enterprise edition) is embedded within Alfresco Process Services and directly used through its Java API.
The REST API has two parts:
The REST API that exposes operations in the context of the applications that are part of Process Services. This REST API is used by the user interface and should be used in most cases.
The REST API that exposes the core engine API directly. Note that this interface is intended for highly custom applications as it exposes the full capabilities and data within the Process Engine. Consequently, a user with the tenant admin or tenant manager role is needed to access this part of the REST API for security reasons.
The application requires Java 7 and is compliant with JEE 6 technologies. The Process Engine itself also supports Java 6, however for components such as Elasticsearch, Alfresco Process Servicesrequires Java 7 or Java 8. Review the Supported Stacks [20] list for more information on supported platforms.
The backend logic specific to the Process Services logic is implemented using Spring 4 and JPA (Hibernate).
All user interfaces are written using HTML5 and AngularJS.
Alfresco Process Services uses the following external systems:
A relational database.
An Elasticsearch installation. Note that the application ships with an embedded Elasticsearch by default.
A file system (shared file system in multi-node setup) where content is stored.
An identity management store such as LDAP or Active Directory (optional). By default, a database-backed user and group store is used.
The Process Engine is managed using the Administrator application. This is also provided as a WAR file.
The App Designer is an Eclipse plugin that can be used by developers to create BPMN 2.0 process definitions within their Eclipse IDE. You can also configure the plugin to pull and push process definition models.
The application can also connect to other on-premise or cloud systems, such as Alfresco Content Services, Box, and Google Drive (not shown in the diagram).
The Maven pom.xml file in this example project can be used to get an overview of all necessary Maven dependencies. The example project also contains the Spring configuration beans that are needed by the Alfresco Process Services components.
The src/main/webapp folder contains all the JavaScript sources of the Alfresco Process Services app in minified format. In addition, you can have access to the full JavaScript source that’s provided in a separate bundle. If the context root of the application is changed, make sure to change the URI configuration in the app-cfg.js file in the src/main/webapp/scripts folder.
The following Maven modules are the most import ones.
The diagram is structured in such a way that the lowest module is a dependency of the module one up higher (and so forth).
All Maven modules have com.activiti as Maven groupId. The version of the artifact is the release version of Process Services.
activiti-app-model : Contains the domain objects, annotated with JPA annotations for persistency and various Spring repositories for executing the actual database operations. Also has the Java pojos of the JSON representations that are used for example as responses by the REST endpoints.
activiti-app-logic : Contains the services and actual BPM Suite logic.
activiti-app-rest : Contains the REST endpoints that are used by the UI and the public API.
activiti-app-dependencies : Contains all the Alfresco Process Services dependencies. It is also a convenient Maven module (packaging type is pom) for development.
activiti-app : Contains configuration classes.
activiti-app-root: Contains the root pom. Do not use this for development.
By default, a file name render-form-extensions.js in the workflow/extensions folder is present and loaded in the index.html file of the workflow folder. It has empty methods by default:
var ALFRESCO = ALFRESCO || {}; ALFRESCO.formExtensions = { // This method is invoked when the form field have been rendered formRendered:function(form, scope) { }, // This method is invoked when input values change (ng-change function) formFieldValueChanged:function(form, field, scope) { }, // This method is invoked when an input field gets focus (focus event with ng-focus function) formFieldFocus:function(form, field, scope) { }, // This method is invoked when an input field has lost focus (blur event with ng-blur function) formFieldBlur:function(form, field, scope) { }, // This method is invoked when a person has been selected in the people picker formFieldPersonSelected:function(form, field, scope) { }, // This method is invoked when an email has been filled-in in the people picker formFieldPersonEmailSelected:function(form, field, scope) { }, // This method is invoked when a person has been removed in the people picker formFieldPersonRemoved:function(form, field, scope) { }, // This method is invoked when a group has been selected in the functional group picker formFieldGroupSelected:function(form, field, scope) { }, // This method is invoked when a group has been removed in the functional group picker formFieldGroupRemoved:function(form, field, scope) { }, // This method is invoked when content has been uploaded in the upload field formFieldContentUploaded:function(form, field, scope) { }, // This method is invoked when content has been removed in the upload field formFieldContentRemoved:function(form, field, scope) { }, // This method is invoked when the REST values or set in a dropdown, radio or typeahead field formFieldRestValuesSet:function(form, field, scope) { }, // This method is invoked when the complete or an outcome button has been clicked and before the task is completed. formBeforeComplete:function(form, outcome, scope) { }, // This method is invoked when input values change (ng-change function) in a dynamic table formTableFieldValueChanged:function(form, field, columnDefinition, editRow, scope) { }, // This method is invoked when an input field gets focus (focus event with ng-focus function) in a dynamic table formTableFieldFocus:function(form, field, columnDefinition, editRow, scope) { }, // This method is invoked when an input field has lost focus (blur event with ng-blur function) in a dynamic table formTableFieldBlur:function(form, field, columnDefinition, editRow, scope) { }, // This method is invoked when the REST values or set in a dropdown field in a dynamic table formTableFieldRestValuesSet:function(form, field, columnDefinition, editRow, scope) { }, // This method is invoked when the form fields have been rendered in the dynamic table popup formTableRendered:function(form, field, columnDefinitions, editRow, scope) { }, // This method is invoked when the complete button has been clicked and before the dynamic table popup is completed. formTableBeforeComplete:function(form, field, editRow, scope) { }, // This method is invoked when the cancel button has been clicked and before the dynamic table popup is cancelled. formTableBeforeCancel:function(form, field, editRow, scope) { }, // This method is invoked when input values change (ng-change function) and will disable the complete buttons when false (boolean) is returned. formValidateFieldValueChanged:function(form, field, scope) { }, // This method is invoked when the complete button has been clicked and will prevent the form completion when false (boolean) is returned. formValidateBeforeSubmit:function(form, outcome, scope) { }, // This method is invoked when input values change (ng-change function) in a dynamic table and will disable the save button when false (boolean) is returned. formTableValidateFieldValueChanged:function(form, field, columnDefinition, editRow, scope) { }, // This method is invoked when the complete button has been clicked and before the dynamic table popup is completed and prevent the form completion // when false (boolean) is returned. formTableValidateBeforeComplete:function(form, field, editRow, scope) { }, // This method is invoked when a task is completed successfully taskCompleted:function(taskId, form, scope) { }, // This method is invoked when a task is completed unsuccessfully taskCompletedError:function(taskId, errorResponse, form, scope) { }, // This method is invoked when a task is saved successfully taskSaved:function(taskId, form, scope) { }, // This method is invoked when a task is saved unsuccessfully taskSavedError:function(taskId, errorResponse, form, scope) { } };
This file can be changed to add custom logic. Alternatively, it is also possible to add new JavaScript files and reference them in the index.html file (do take those files in account when upgrading to newer versions of the application) but it is also possible to load additional folders using the resource loader, see Custom web resources [22].
In every event method the full form variable is passed as a parameter. This form variable contains the form identifier and name, but also the full set of form fields with type and other configuration information.
In addition the changed field is passed when applicable and the Angular scope of the form renderer is also included. This is a regular Angular directive (that is, isolated) scope, with all methods available.
For example, to get the current user:
formRendered:function(form, scope) { var currentUser = scope.$root.account; console.log(currentUser); }
Form stencils are defined in the Stencils section of the App Designer. A new form field type consists of the following:
An HTML template that is rendered when drag and dropping from the palette on the form canvas is the form builder.
An HTML template that is rendered when the form is displayed at run-time.
An optional custom AngularJS controller in case custom logic needs to be applied to the form field.
An optional list of third party scripts that are needed when working with the form field at run-time.
*Create a new form stencil in the App Designer and click the Add new item link.
The Form run-time template (the HTML used when the form is rendered at run-time) and the Form editor template (the HTML used in the form builder) is the same here:
<img src="http://activiti.org/images/activiti_logo.png"></img>
The Form runtime template needs to show the image that the form builder has selected. Assume that a property url is set (see later on). Note the use of ng-src (see AngularJs docs on ng-src [26]) to have a dynamic image:
<img ng-src="{{field.params.customProperties.url}}"></img>
Note the syntax field.params.customProperties to get access to the non-default properties of the form field.
The Form editor template simply needs to be a generic depiction of an image or even simpler like here, just a bit of text
<i>The custom image here</i>
Don’t forget to add a property url to this stencil item with the name url and type text.
We’ll use the Epoch library [27] as an example here. Download the following files from its Github site:
d3.min.js [28]
epoch.min.js [29]
Create a new form stencil item and name it "Chart". Scroll down to the Script library imports section, and upload the two libraries. At run-time, these third party libraries will be included when the form is rendered.
Note: The order in which the third party libraries are defined is important. Since the Epoch library depends on d3, d3 needs to be first in the table and epoch second (as that is the order in which they are loaded at run-time).
The Form editor template is the easy part. We could just use an image of a pie chart here.
<img src="url_to_pie_chart_image.png"></img>
First define the controller for this form field type. The controller is an AngularJs controller, that does mainly three things:
Keep a model of the line items
Implement a callback for the button that can be clicked
Store the value of the form field in the proper format of Alfresco Process Services
angular.module('activitiApp') .controller('MyController', ['$rootScope', '$scope', function ($rootScope, $scope) { console.log('MyController instantiated'); // Items are empty on initialisation $scope.items = []; // The variable to store the piechart data (non angular) var pieChart; // Epoch can't use the Angular model, so we need to clean it // (remove hashkey etc, specific to Angular) var cleanItems = function(items) { var cleanedItems = []; items.forEach(function(item) { cleanedItems.push( { label: item.label, value: item.value} ); }); return cleanedItems; }; // Callback for the button $scope.addItem = function() { // Update the model $scope.items.push({ label: 'label ' + ($scope.items.length + 1), value: 0 }); // Update the values for the pie chart // Note: Epoch is not an angular lib so doesn't use the model directly if (pieChart === undefined) { pieChart = jQuery('.activiti-chart-' + $scope.field.id).epoch({ type: 'pie', data: cleanItems($scope.items) }); console.log('PieChart created'); } else { $scope.refreshChart(); } }; // Callback when model value changes $scope.refreshChart = function() { pieChart.update(cleanItems($scope.items)); console.log('PieChart updated'); }; // Register this controller to listen to the form extensions methods $scope.registerCustomFieldListener(this); // Deregister on form destroy $scope.$on("$destroy", function handleDestroyEvent() { console.log("destroy event"); $scope.removeCustomFieldListener(this); }); // Setting the value before completing the task so it's properly stored this.formBeforeComplete = function(form, outcome, scope) { console.log('Before form complete'); $scope.field.value = JSON.stringify(cleanItems($scope.items)); }; // Needed when the completed form is rendered this.formRendered = function(form, scope) { console.log(form); form.fields.forEach(function(field) { if (field.type === 'readonly' && $scope.field.id == field.id && field.value && field.value.length > 0) { $scope.items = JSON.parse(field.value); $scope.isDisabled = true; pieChart = jQuery('.activiti-chart-' + $scope.field.id).epoch({ type: 'pie', data: cleanItems($scope.items) }); } }); }; }]);
The Form runtime template needs to reference this controller, use the model and link the callback for the button:
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/epoch/0.6.0/epoch.min.css"> <div ng-controller="MyController" style="float:left;margin: 35px 20px 0 0;"> <div ng-repeat="item in items"> <input type="text" ng-model="item.label" style="width:200px; margin: 0 10px 10px 0;" ng-change="refreshChart()"> <input type="number" ng-model="item.value" style="width: 80px; margin-bottom: 10px;" ng-change="refreshChart()"> </div> <div> <button class="btn btn-default btn-sm" ng-click="addItem()" ng-disabled="isDisabled"> Add item </button> </div> </div> <div class="epoch category10" ng-class="'activiti-chart-' + field.id" style="display:inline-block;width: 200px; height: 200px;"></div> <div class="clearfix"></div>
At run-time, the following will be rendered:
Following is an example of a new resource section in the app-cfg.js file located in the tomcat/webapps/activiti-app/scripts folder:
ACTIVITI.CONFIG.resources = { '*': [ { 'tag': 'link', 'rel': 'stylesheet', 'href': ACTIVITI.CONFIG.webContextRoot + '/custom/style.css?v=1.0' } ], 'workflow': [ { 'tag': 'script', 'type': 'text/javascript', 'src': ACTIVITI.CONFIG.webContextRoot + '/custom/javascript.js?v=1.0' } ] };
The ACTIVITI.CONFIG.resources object makes it possible to load different files for each of the Activiti applications using their names as key for a list of additional resources that shall be loaded, the different app names are: landing, analytics, editor, idm and workflow. The * key means that a default resource list will be used unless there is a specific config key for the app being loaded.
For example, if a user would enter the editor app, with the config above deployed, custom/style.css would be the only custom resource that would be loaded. If a user would go to the workflow app, custom/javascript.js would be the only custom resource that would be loaded. So if workflow also wants to load the custom/style.css that would have to be specified again inside the workflow resource list.
A document template can be:
Tenant wide: Anyone can use this template in their processes. Useful for company templates.
Process model specific: This template is uploaded while modeling the process model, and is bound to the lifecycle of the process model.
When exporting an App model, process model document templates are included by default and are uploaded again on import. Tenant document templates are not exported, however matched by the document template name as names are unique for tenant document templates.
In the .docx template, you can insert process variables using the following syntax:
<<[myVariable]>>
Since the above method does not perform null checks, an exception will be thrown at run-time if the variable is null. Therefore, use the following method to prevent such errors:
<<[variables.get("myVariable")]>>
If this variable is null, a default value will be inserted instead. You can also provide a default value:
<<[variables.get("myVariable", "myDefaultValue")]>>
Note: Form field types such as Dropdown, Radio button, and Typeahead use myVariable_ID for ID and myVariable_LABEL for label value. The ID is the actual value used by service tasks and are inserted by default. To display the label value in the generated document, use myVariable_LABEL.
The document generation method uses libraries provided by Aspose in the back-end.
When using the Generate Document task, make sure that you use the correct syntax for your variables and expressions. Surround your variables with <<[..]>> characters. For example:
<<[variableid]>>
<<[variables.get("variableid")]>>
<<[variables.get("variableid","adefaultifnull")
Some more examples:
If/else conditional blocks:
Text type: <<if [textfield==day]>> AM, <<else>> PM \<</if>>
Amount type: <<if [annualsalary > $40000]>>, it is generous, <<else>> a standard starting salary \<</if>>
Checkbox: <<if [senstitiveflag=="true"]>>it is Confidential, <<else>> Not Confidential \<</if>>
Date type: <<[datefield]>>
Format date type: <<[datefield]>>:"yyyy.MM.dd">>
Number/amount: <<[amountfield]>>
String Boolean: <<[Genericcheckbox]>>
Radio button / Typehead / dropdown: Select <<[Options_LABEL]>> with an ID <<[Options_ID]>>
The audit log is also generated the same way.
For example, the following snippet from the template shows advanced constructs:
It is also possible to have custom Spring bean that processes the process variables just before rendering the document, Processing document generation variables [30].
To build against a specific version of Alfresco Process Services, add the following dependency to your Maven pom.xml file:
<dependencies> <dependency> <groupId>com.activiti</groupId> <artifactId>activiti-app-logic</artifactId> <version>${suite.version}</version> </dependency> </dependencies>
package my.company; import org.activiti.engine.delegate.DelegateExecution; import org.activiti.engine.delegate.JavaDelegate; public class MyJavaDelegate implements JavaDelegate { public void execute(DelegateExecution execution) throws Exception { System.out.println("Hello from the class delegate"); execution.setVariable("var1", "Hello from the class delegate"); } }
Build a jar with this class, and add it to the classpath. In the Service task configuration, set the class property to using the fully qualified classname (in this case my.company.MyJavaDelegate).
package com.activiti.extension.bean; import org.activiti.engine.impl.pvm.delegate.ActivityExecution; import org.springframework.stereotype.Component; @Component("helloWorldBean") public class HelloWorldBean { public void sayHello(ActivityExecution execution) { System.out.println("Hello from " + this); execution.setVariable("var3", " from the bean"); } }
Build a jar with this class, and add it to the classpath. To use this bean in a service task, set the expression property to ${helloWorldBean.sayHello(execution)}.
package com.activiti.extension.conf; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CustomConfiguration { @Bean public SomeBean someBean() { return new SomeBean(); } }
package com.activiti.extension.bean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.activiti.extension.conf.SomeBean; @Component("helloWorldBeanWithInjection") public class HelloWorldBeanWithInjection { @Autowired private SomeBean someBean; public void sayHello() { System.out.println(someBean.getValue()); } }
To get the current user, it is possible to use the com.activiti.common.security.SecurityUtils helper class.
Bean Whitelisting
By default, you can specify any Spring bean for use in an expression. While this provides ease of use (since any beans you develop will be automatically scanned for as described above), it also increases the possibilities of misuse and security threats. To help prevent these issues from happening, you can whitelist Spring beans by making the following changes:
beans.whitelisting.enabled=true
activiti-app/WEB-INF/classes/activiti/beans-whitelist.conf
Example usage of bean whitelisting:
${execution.setVariable('userCount', userService.getUserCount())}
If beans.whitelisting.enabled is set to false or the property is missing, the process is completed and the Display Text field should show the value of the usercount variable.
To complete the process successfully using bean whitelisting, you must set beans.whitelisting.enabled to true and add the bean name to beans-whitelist.conf:
# list bean names that should be whitelisted userService
Service Task Class Whitelisting
This provides an alternative to bean whitelisting that enables more fine-grained control over what a developer can execute. For example, you can configure which patterns you allow to be executed using expressions.
You can also whitelist full class names or package patterns such as 'com.activiti.*'.
To whitelist service task classes, do the following:
class.whitelisting.enabled=true
Whitelisting Scripting Languages
#Here you can specify which script types are allowed to be executed javascript js ecmascript groovy juel
Class whitelisting in JavaScript
javascript.secure-scripting.enabled=true
javascript.secure-scripting.enable-class-whitelisting = true
java.lang.System java.util.ArrayList org.apache.tomcat.util.log.SystemLogHandler
The auditLogBean can be used to generate audit logs in .pdf format for a completed process instance or a completed task. The log will be saved as a field value for the process and the task (if a task audit log is generated).
The following code can be used in the expression of a service task to generate a process instance audit log named My first process instance audit log. The third argument determines if the current date shall be appended to the file name. The pdf will be associated with the process field myFieldName.
${auditLogBean.generateProcessInstancePdf(execution, My first process instance audit log, true, myFieldName)}
To create a task audit log named My first task audit log add the following expression to the "complete" event in a task listener. Again the third argument determines if the current date shall be appended to the file name. The pdf will be associated with the field myFieldName.
${auditLogBean.generateTaskPdf(task, My first task audit log, true, myFieldName)}
You can view the audit logs from the My Tasks app by clicking the "Audit Log" link when viewing the details of a completed process or task. When doing so the following two rest calls are made.
Process instance audit log:
GET app/rest/process-instances/{process-instance-id}/audit
Task audit log:
GET app/rest/tasks/{task-id}/audit
The documentMergeBean can be used to merge the content of multiple documents (files of type .doc or .docx) from a process into a single document which will be become the value of a provided process variable. The file name of the new document will be set to the file name of the first field in the list followed by the string "_merged" and the suffix from the same field.
In the following example, the content of myFirstField and mySecondField will be merged into a new document with the field ID set to myFirstField and the filename set to:
<filename-from-myFirstField>_merged.<filenameSuffix-from-myFirstFields>
The new document will become the value of a process variable named myProcessVariable.
${documentMergeBean.mergeDocuments(myFirstField;mySecondField, myProcessVariable, execution)}
The emailBean can be used to retrieve the email of the current user or the process initiator.
To get the email of the current user use the following expression where 123 is the userId:
${emailBean.getEmailByUserId(123, execution)}
To get the email of the process initiator use the following expression:
${emailBean.getProcessInitiator(execution)}
The userInfoBean makes it possible to get access to general information about a user or just the email of a user.
To get general information about a user (the data that can be found in com.activiti.domain.idm.User), use the following expression where userId is the database ID of the user and can be supplied either as a Long or a String.
${userInfoBean.getUser(123, execution)}
To get the email of a user use the following expression where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getEmail(123, execution)}
To get the first name of a user use the following expression where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getFirstName(123, execution)}
To get the last name of a user use the following expression where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getLastName(123, execution)}
To get both first name and last name of a user use the following expression where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getFullName(123, execution)}
To get a user object representing the current user use the following expression where the returned value is an instance of LightUserRepresentation containing fields like id, firstName, lastName, email, externalId, pictureId.
${userInfoBean.getCurrentUser()}
To get a user’s primary group name use the following expression where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getPrimaryGroupName(123)}
To get a group object representing a user’s primary group use the following expression where the return value is an instance of LightGroupRepresentation, containing id, name, externalId and status, and where 123 is the database id of the user and can be supplied either as a Long or a String.
${userInfoBean.getPrimaryGroup(123)}
interface: com.activiti.api.security.LoginListener and com.activiti.api.security.LogoutListener
Maven module: activiti-app-logic
An implementation of this class will get a callback when a user logs in or logs out.
Example:
package com.activiti.extension.bean; @Component public class MyLoginListener implements LoginListener { private static final Logger logger = LoggerFactory.getLogger(GfkLoginListener.class); public void onLogin(User user) { logger.info("User " + user.getFullName() + " has logged in"); } }
interface: com.activiti.api.engine.ProcessEngineConfigurationConfigurer
Maven module: activiti-app-logic
An implementation of this class will get called when the Activiti process engine configuration is initialized, but before the process engine is built. This allows for customization to the process engine configuration.
Example:
@Component public class MyProcessEngineCfgConfigurer implements ProcessEngineConfigurationConfigurer { public void processEngineConfigurationInitialized( SpringProcessEngineConfiguration springProcessEngineConfiguration) { ... // Tweaking the process engine configuration } }
interface: com.activiti.api.engine.DmnEngineConfigurationConfigurer
Maven module: activiti-app-logic
An implementation of this class will get called when the Process Services rule engine configuration is initialized, but before the process engine is built. This allows for customization to the rule engine configuration.
Example:
@Component public class MyDmnEngineCfgConfigurer implements DmnEngineConfigurationConfigurer { public void dmnEngineConfigurationInitialized(DmnEngineConfiguration dmnEngineConfiguration) { ... // Tweaking the rule engine configuration } }
It is possible to listen to events fired by the Process Engine. By default (and if enabled) there is a listener that captures these events, processes them before sending them to Elasticsearch (which is used for analytics). If the event data should be going somewhere else, for example an external BI warehouse, the following interface should be implemented and can be used to execute any logic when the event is fired.
See the example apps folder that comes with Alfresco Process Services. It has a jdbc-event-listener folder, in which a Maven project can be found that captures these events and stored them relationally in another database.
interface: com.activiti.service.runtime.events.RuntimeEventListener
Maven module: activiti-app-logic
All implementations exposing this interface will be injected into the process engine at run time.
Example:
package com.activiti.extension.bean; import com.activiti.service.runtime.events.RuntimeEventListener; import org.activiti.engine.delegate.event.ActivitiEvent; @Component public class PostgresEventListener implements RuntimeEventListener { @Override public boolean isEnabled() { return true; } @Override public void onEvent(ActivitiEvent activitiEvent) { // TODO: handle event here } @Override public boolean isFailOnException() { return false; } }
interface: com.activiti.api.docgen.TemplateVariableProcessor
Maven module: activiti-app-logic
This section describes the implementation of the document generation task for generating a document based on a MS Word docx template.
An implementation of this class will get called before the variable is passed to the template processor, making it possible to change the value that will be used as the variable name in the template.
Example:
@Component public class MyTemplateVariableProcessor implements TemplateVariableProcessor { public Object process(RuntimeDocumentTemplate runtimeDocumentTemplate, DelegateExecution execution, String variableName, Object value) { return value.toString() + "___" + "HELLO_WORLD"; } }
Using the above example, you can add "HELLO_WORLD" to all variable usages in the template. However, you can also add sophisticated implementations based on process definition lookup using the process definition ID from the execution and inject the RepositoryService in your bean.
Use the business calendar when calculating due dates for tasks.
You can override the default business calendar implementation, for example, to include bank holidays, company holidays, and so on. To override the default implementation, add a Spring bean implementing the com.activiti.api.calendar.BusinessCalendarService to the classpath with the @Primary notation.
Check the Javadoc on the BusinessCalendarService for more information.
@Primary @Service public class MyBusinessCalendarService implements BusinessCalendarService { ... }
Below is an example implementation that takes weekend days into account when calculating due dates.
@Primary @Service public class SkipWeekendsBusinessCalendar implements BusinessCalendarService { protected static final int DAYS_IN_WEEK = 7; protected List<Integer> weekendDayIndex; protected DateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy"); public SkipWeekendsBusinessCalendar() { // add Saturday and Sunday as weekend days weekendDayIndex.add(6); weekendDayIndex.add(7); } public Date addToDate(Date date, int years, int months, int days, int hours, int minutes, int seconds) { return calculateDate(new DateTime(date), years, months, days, hours, minutes, seconds, 1); } public Date subtractFromDate(Date date, int years, int months, int days, int hours, int minutes, int seconds) { return calculateDate(new DateTime(date), years, months, days, hours, minutes, seconds, -1); } protected Date calculateDate(DateTime relativeDate, int years, int months, int days, int hours, int minutes, int seconds, int step) { // if date is on a weekend skip to a working day relativeDate = skipWeekEnds(relativeDate, step); Period period = new Period(years, months, 0, days, hours, minutes, seconds, 0); // add weekends to period period = period.plusDays(countWeekEnds(relativeDate, period, step)); // add/subtract period to get the final date, again if date is on a weekend skip to a working day return skipWeekEnds(addPeriod(relativeDate, period, step), step).toDate(); } protected DateTime addPeriod(DateTime relativeDate, Period period, int step) { if (step < 0) { return relativeDate.minus(period); } return relativeDate.plus(period); } protected DateTime skipWeekEnds(DateTime relativeDate, int step) { while(weekendDayIndex.contains(relativeDate.getDayOfWeek())) { relativeDate = relativeDate.plusDays(step); } return relativeDate; } protected int countWeekEnds(DateTime relativeDate, Period period, int step) { // get number of days between two dates int days = Math.abs(Days.daysBetween(relativeDate, addPeriod(relativeDate, period, step)).getDays()); int count = 0; for(int weekendDay : weekendDayIndex) { count+=countWeekDay(relativeDate, weekendDay, days, step); } return count; } protected int countWeekDay(DateTime relativeDate, int weekDay, int days, int step) { int count = 0; DateTime dt = relativeDate.toDateTime(); // if date's day of week is not the target day of week // skip to target day of week if(weekDay != relativeDate.getDayOfWeek()) { int daysToSkip = 0; if (step > 0) { if (weekDay > relativeDate.getDayOfWeek()) { daysToSkip = weekDay - relativeDate.getDayOfWeek(); } else { daysToSkip = weekDay - relativeDate.getDayOfWeek() + DAYS_IN_WEEK; } } else { if (weekDay > relativeDate.getDayOfWeek()) { daysToSkip = Math.abs(weekDay - relativeDate.getDayOfWeek() - DAYS_IN_WEEK); } else { daysToSkip = relativeDate.getDayOfWeek() - weekDay; } } // return if target day of week is beyond range of days if (daysToSkip > days) { return 0; } count++; dt = dt.plusDays(daysToSkip * step); days-=daysToSkip; } if (days>=DAYS_IN_WEEK) { dt = dt.plusDays(days * step); count+=(Weeks.weeksBetween(relativeDate, dt).getWeeks() * step); } return count; } @Override public DateFormat getStringVariableDateFormat() { return dateFormat; }
The REST API is built using Spring MVC. Please check the Spring MVC documentation [47] on how to create new Java beans to implement REST endpoints.
To build against the REST logic of Process Services and its specific dependencies, add following dependency to your Maven pom.xml file:
<dependencies> <dependency> <groupId>com.activiti</groupId> <artifactId>activiti-app-rest</artifactId> <version>${suite.version}</version> </dependency> </dependencies>
The bean needs to be in the com.activiti.extension.rest package to be found.
A very simple example is shown below. Here, the Process Services TaskService is injected and a custom response is fabricated. Of course, this logic can be anything.
package com.activiti.extension.rest; import com.activiti.domain.idm.User; import com.activiti.security.SecurityUtils; import org.activiti.engine.TaskService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/rest/my-rest-endpoint") public class MyRestEndpoint { @Autowired private TaskService taskService; @RequestMapping(method = RequestMethod.GET, produces = "application/json") public MyRestEndpointResponse executeCustonLogic() { User currentUser = SecurityUtils.getCurrentUserObject(); long taskCount = taskService.createTaskQuery().taskAssignee(String.valueOf(currentUser.getId())).count(); MyRestEndpointResponse myRestEndpointResponse = new MyRestEndpointResponse(); myRestEndpointResponse.setFullName(currentUser.getFullName()); myRestEndpointResponse.setTaskCount(taskCount); return myRestEndpointResponse; } private static final class MyRestEndpointResponse { private String fullName; private long taskCount; // Getters and setters } }
Create a jar containing this class, and add it to the classpath.
A class like this in the com.activiti.extension.rest package will be added to the rest endpoints for the application (e.g. for use in the UI), which use the cookie approach to determine the user. The url will be mapped under /app. So, if logged in into the UI of the BPM Suite, one could go to http://localhost:8080/activiti-app/app/rest/my-rest-endpoint and see the result of the custom rest endpoint:
{"fullName":" Administrator","taskCount":8}
To add a custom REST endpoint to the public REST API, protected by basic authentication, a similar class should be placed in the com.activiti.extension.api package:
package com.activiti.extension.api; import com.activiti.domain.idm.User; import com.activiti.security.SecurityUtils; import org.activiti.engine.TaskService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/enterprise/my-api-endpoint") public class MyApiEndpoint { @Autowired private TaskService taskService; @RequestMapping(method = RequestMethod.GET, produces = "application/json") public MyRestEndpointResponse executeCustonLogic() { User currentUser = SecurityUtils.getCurrentUserObject(); long taskCount = taskService.createTaskQuery().taskAssignee(String.valueOf(currentUser.getId())).count(); MyRestEndpointResponse myRestEndpointResponse = new MyRestEndpointResponse(); myRestEndpointResponse.setFullName(currentUser.getFullName()); myRestEndpointResponse.setTaskCount(taskCount); return myRestEndpointResponse; } private static final class MyRestEndpointResponse { private String fullName; private long taskCount; // Getters and setters } }
Note that the endpoint needs to have /enterprise as first element in the url, as this is configured in the SecurityConfiguration to be protected with basic authentication (more specific, the api/enterprise/* is).
Which can be accessed like the regular API:
> curl -u admin@app.activiti.com:password http://localhost:8080/activiti-app/api/enterprise/my-api-endpoint > {"fullName":" Administrator","taskCount":8}
Note: Due to classloading, it is currently not possible to put jars with these custom rest endpoints in the global or common classpath (for example tomcat/lib for Tomcat). They should be put in the web application classpath (for example WEB-INF/lib).
The provided custom methods can be overridden by your own custom expression functions or custom methods can be added. This is possible via a hook point in the rule engine configuration (see Rule engine configuration configurer [48]).
You can configure the Engine with additional expression functions by implementing CustomExpressionFunctionRegistry.
interface: com.activiti.dmn.engine.impl.mvel.config.CustomExpressionFunctionRegistry
Maven module: activiti-dmn-engine
Example:
import com.activiti.dmn.engine.CustomExpressionFunctionRegistry; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @Component public class MyCustomExpressionFunctionsRegistry implements CustomExpressionFunctionRegistry { public Map<String, Method> getCustomExpressionMethods() { Map<String,Method> myCustomExpressionMethods = new HashMap<>(); try { String expressionToken = "dosomething"; Method customExpressionMethod = SomeClass.class.getMethod("someMethod", String.class); myCustomExpressionMethods.put(expressionToken, customExpressionMethod); } catch (NoSuchMethodException e) { // handle exception } return myCustomExpressionMethods; } }
This registry must be provided to the rule engine configuration using the hook point (see Rule engine configuration configurer [48]).
This example adds the expression function from the example above to the default custom expression functions.
Example:
import com.activiti.dmn.engine.DmnEngineConfiguration; import org.springframework.beans.factory.annotation.Autowired; public class MyDmnEngineCfgConfigurer implements DmnEngineConfigurationConfigurer { @Autowired MyCustomExpressionFunctionsRegistry myExpressionFunctionRegistry; public void dmnEngineConfigurationInitialized(DmnEngineConfiguration dmnEngineConfiguration) { dmnEngineConfiguration.setPostCustomExpressionFunctionRegistry( myExpressionFunctionRegistry); } }
Overriding the default custom expression functions can be done by:
dmnEngineConfiguration.setCustomExpressionFunctionRegistry( myExpressionFunctionRegistry);
You can create Custom Data Models that connect to external sources and perform custom data operations when working with entity objects.
Implement AlfrescoCustomDataModelService to manage operations such as insert, update, and select data in Custom Data Models.
interface: com.activiti.api.datamodel.AlfrescoCustomDataModelService
maven module: activiti-app-logic
To implement the AlfrescoCustomDataModelService interface:
Note that it should be in a package that can be scanned, such as com.activiti.extension.bean.
package com.activiti.extension.bean; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.activiti.api.datamodel.AlfrescoCustomDataModelService; import com.activiti.model.editor.datamodel.DataModelDefinitionRepresentation; import com.activiti.model.editor.datamodel.DataModelEntityRepresentation; import com.activiti.runtime.activiti.bean.datamodel.AttributeMappingWrapper; import com.activiti.variable.VariableEntityWrapper; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @Service public class AlfrescoCustomDataModelServiceImpl implements AlfrescoCustomDataModelService { @Autowired protected ObjectMapper objectMapper; @Override public String storeEntity(List<AttributeMappingWrapper> attributeDefinitionsAndValues, DataModelEntityRepresentation entityDefinition, DataModelDefinitionRepresentation dataModel) { // save entity data and return entity id } @Override public ObjectNode getMappedValue(DataModelEntityRepresentation entityValue, String mappedName, Object variableValue) { // fetch entity data and return as an ObjectNode } @Override public VariableEntityWrapper getVariableEntity(String keyValue, String variableName, String processDefinitionId, DataModelEntityRepresentation entityValue) { // fetch entity data and return as a VariableEntityWrapper } }
This implementation of AlfrescoCustomDataModelServiceImpl class is called, for example, when a select, insert, or update operation on a custom data model is performed.
Custom reports have full access to the Elasticsearch indexes generated by Alfresco Process Services when it is enabled.
See Administering [1] for details on how to configure events to be sent to Elasticsearch.
The following section assumes that you have a reasonable understanding of what Elasticsearch is and an understanding of indexes, types and type mappings. The Elasticsearch Definitive Guide [49] is a great learning resource if you are new to the engine and there is also a Reference Guide [50] which you should find helpful to refer to as you start using it directly yourself.
A custom report is a custom section available in the Analytics app and also within each published app, which shows one or more custom reports.
Each report is implemented by a Spring bean which is responsible for two things:
The UI will automatically display the correct widgets based on the data that your bean sends.
Your Spring bean will be discovered automatically via annotations but must be placed under the package com.activiti.service.reporting. Since this package is used for the out-of-the-box reports it is recommended that custom reports use the sub-package such as com.activiti.service.reporting.custom.
The overall structure of the class will be as follows, for the full source please see the web link at the end of this section.
package com.activiti.service.reporting.custom; import com.activiti.domain.reporting.ParametersDefinition; import com.activiti.domain.reporting.ReportDataRepresentation; import com.activiti.service.api.UserCache; import com.activiti.service.reporting.AbstractReportGenerator; import org.activiti.engine.ProcessEngine; import org.elasticsearch.client.Client; import org.springframework.stereotype.Component; import java.util.Map; @Component(CustomVariablesReportGenerator.ID) public class CustomVariablesReportGenerator extends AbstractReportGenerator { public static final String ID = "report.generator.fruitorders"; public static final String NAME = "Fruit orders overview"; @Override public String getID() { return ID; } @Override public String getName() { return NAME; } @Override public ParametersDefinition getParameterDefinitions(Map<String, Object> parameterValues) { return new ParametersDefinition(); } @Override public ReportDataRepresentation generateReportData(ProcessEngine processEngine, Client elasticSearchClient, String indexName, UserCache userCache, Map<String, Object> parameterMap) { ReportDataRepresentation reportData = new ReportDataRepresentation(); // Perform queries and add report data here return reportData; }
You must implement the generateReportData() method which is declared abstract in the superclass, and you can choose to override the getParameterDefinitions() method if you need to collect some user-selected parameters from the UI to use in your query.
The generateReportData() method of your bean is responsible for two things:
Perform one or more ElasticSearch queries to fetch report data
Populate chart/table data from the query results
A protected helper method executeSearch() is provided which provides a concise syntax to execute an ElasticSearch search query given a query and optional aggregation, the implementation of which also provides logging of the query generated by the Java client API before it is sent. This can help with debugging your queries using Sense, or assist you in working out why the Java client is not generating the query you expect.
return executeSearch(elasticSearchClient, indexName, ElasticSearchConstants.TYPE_VARIABLES, new FilteredQueryBuilder( new MatchAllQueryBuilder(), FilterBuilders.andFilter( new TermFilterBuilder("processDefinitionKey", PROCESS_DEFINITION_KEY), new TermFilterBuilder("name._exact_name", "customername") ) ), AggregationBuilders.terms("customerOrders").field("stringValue._exact_string_value") );
The log4j configuration required to log queries being sent to ElasticSearch via executeSearch() is as follows
log4j.logger.com.activiti.service.reporting.AbstractReportGenerator=DEBUG
Alternatively you can manually execute any custom query directly via the Client instance passed to the generateReportData() method, for example:
return elasticSearchClient .prepareSearch(indexName) .setTypes(ElasticSearchConstants.TYPE_PROCESS_INSTANCES) .setQuery(new FilteredQueryBuilder(new MatchAllQueryBuilder(), applyStatusProcessFilter(status))) .addAggregation( new TermsBuilder(AGGREGATION_PROCESS_DEFINITIONS).field(EventFields.PROCESS_DEFINITION_ID) .subAggregation(new FilterAggregationBuilder(AGGREGATION_COMPLETED_PROCESS_INSTANCES) .filter(new ExistsFilterBuilder(EventFields.END_TIME)) .subAggregation(new ExtendedStatsBuilder(AGGREGATION_STATISTICS).field(EventFields.DURATION))));
Generating chart data from queries can be accomplished easily using the converters in the com.activiti.service.reporting.converters package. This avoids the need to iterate over returned query results in order to populate chart data items.
Initially two converters AggsToSimpleChartBasicConverter and AggsToMultiSeriesChartConverter are provided to populate data for pie charts (which take a single series of data) and bar charts (which take multiple series) respectively. These two classes are responsible for iterating over the structure of the ES data, while the member classes of com.activiti.service.reporting.converters.BucketExtractors are responsible for extracting an actual value from the buckets returned in the data.
ReportDataRepresentation reportData = new ReportDataRepresentation(); PieChartDataRepresentation pieChart = new PieChartDataRepresentation(); pieChart.setTitle("No. of orders by customer"); pieChart.setDescription("This chart shows the total number of orders placed by each customer"); new AggsToSimpleChartBasicConverter(searchResponse, "customerOrders").setChartData( pieChart, new BucketExtractors.BucketKeyExtractor(), new BucketExtractors.BucketDocCountExtractor() ); reportData.addReportDataElement(pieChart); SingleBarChartDataRepresentation chart = new SingleBarChartDataRepresentation(); chart.setTitle("Total quantities ordered per month"); chart.setDescription("This chart shows the total number of items that were ordered in each month"); chart.setyAxisType("count"); chart.setxAxisType("date_month"); new AggsToMultiSeriesChartConverter(searchResponse, "ordersByMonth").setChartData( chart, new BucketExtractors.DateHistogramBucketExtractor(), new BucketExtractors.BucketAggValueExtractor("totalItems") ); reportData.addReportDataElement(chart);
For more details see the full source on the activiti-custom-reports [52] GitHub project.
Alfresco Process Services uses an HTTP cookie to store a user session. You can use multiple cookies for different browsers and devices. The application uses a database table to store the cookie values (called tokens internally), to allow a shared persistent session store in a multi-node setup.
It’s possible to change the settings regarding cookies:
Property |
description |
default |
---|---|---|
security.cookie.max-age |
The maximum age of a cookie, expressed in seconds. The max-age determines the period in which the browser will send the cookie with the requests. |
2678400 (31 days) |
security.cookie.refresh-age |
The age of a cookie before it is refreshesd. Refreshing means a new token will be created and a new cookie will be returned which the browser will use for subsequent requests. Setting the refresh-age low, will result in many new database rows when the user is using the application.To avoid that a user is suddenly logged out when using the application when reaching the max-age above, tokens are refreshed after this period (expressed in seconds). |
86400 (1 day) |
By default, cookies will have the secure flag set, when the request being made is HTTPS. If you only want to use the remember-me cookie over HTTPS (i.e. make the secure flag mandatory), set the following property to true:
Property |
default |
---|---|
security.cookie.always-secure |
false |
To avoid that the persistent token table gets too full, a background job periodically removes obsolete cookie token values. Possible settings:
Property |
description |
default |
---|---|---|
security.cookie.database-removal.max-age |
The maximum age an entry in the database needs to have to be removed. |
Falls back to the security.cookie.max-age setting if not found. This effectively means that cookies which are no longer valid could be removed immediately from the database table. |
security.cookie.database-removal.cronExpression |
The cron expression determining when the obsolete database table entries for the cookie values will be checked for removal. |
0 0 1 * * ? (01:00 at night) |
Alfresco Process Services needs user, group, and membership information in its database. The main reason is performance (for example quick user/group searches) and data consistency (for example models are linked to users through foreign keys). In the Process Services logic, this is typically referred to as Identity Management (IDM).
Out of the box, all IDM data is stored directly in the database. So when you create a user or group as a tenant administrator, the data ends up in the database tables.
However, typically, the users/groups of a company are managed in a centralized data store such as LDAP (or Active Directory). Process Services can be configured to connect to such a server and synchronize the IDM data to the database table.
See External Identity Management (LDAP/Active Directory) [53] for more information on how to set this up. The basic idea behind it is that the LDAP server will periodically be polled and the IDM data in the database tables will be synchronized: created, updated or deleted depending on what the LDAP server returns and what currently is in the database tables.
This section describes what is needed to have a similar synchronization of IDM data coming from another source. The com.activiti.service.idm.LdapSyncService responsible for synchronizing IDM data from an LDAP/Active Directory store, uses the same hook points as the ones described below and can thus be seen as an advanced example.
Create a simple example synchronization service that demonstrates clearly the concepts and classes to be used. In this example, use a simple text file to represent our external IDM source. The users.txt looks as follows (each line is a user and user data is separated by semi-colons):
jlennon;John;Lennon;john@beatles.com;johnpassword;10/10/2015 rstarr;Ringo;Starr;ringo@beatles.com;ringopassword;11/10/2015 gharrison;George;Harrison;george@beatles.com;georgepassword;12/10/2015 pmccartney;Paul;McCartney;paul@beatles.com;paulpassword;13/10/2015
The groups.txt file is similar (the group name followed by the member ids and a timestamp):
beatles:jlennon;rstarr;gharrison;pmccartney:13/10/2015 singers:jlennon;pmccartney:17/10/2015
The application expects one instance implementing the com.activiti.api.idm.ExternalIdmSourceSyncService interface to be configured when synchronizing with an external IDM source. This interface requires a few methods to either synchronous or asynchronous do a full or differential sync. In a full sync, all data is looked at and compared. A differential sync only returns what has changed since a certain date. The latter is of course used for performance reasons. For example, the default settings for LDAP do a full sync every night and a differential sync every four hours.
You can also implement the com.activiti.api.idm.ExternalIdmSourceSyncService interface directly, but there is an easier way: all the logic to fetch data from the tables, compare, create, update or delete users, groups or membership is encapsulated in the com.activiti.api.idm.AbstractExternalIdmSourceSyncService class. It is advised to extend this class when creating a new external source synchronization service, as in that case the only logic that needs to be written is the actual fetching of the IDM data from the external source.
Create a FileSyncService class. Note the package, com.activiti.extension.bean, which is automatically component scanned. The class is annotated with @Component (@Service would also work).
package com.activiti.extension.bean; @Component public class FileSyncService extends AbstractExternalIdmSourceSyncService { ... }
The com.activiti.api.idm.ExternalIdmSourceSyncService defines the different abstract methods that can be implemented. For example:
The additionalPostConstruct() method will be called after the bean is constructed and the dependencies are injected.
protected void additionalPostConstruct() { // Nothing needed now }
It’s the place to add additional post construction logic, like reading properties from the configuration file. Note the env variable is available for that, which is a standard org.springframework.core.env.Environment instance:
protected void additionalPostConstruct() { myCustomConfig = env.getProperty("my.custom.property"); }
The getIdmType() method simply returns a String identifying the external source type. It is used in the logging that is produced when the synchronization is happening.
protected String getIdmType() { return "FILE"; }
The isFullSyncEnabled(Long tenantId) and isDifferentialSyncEnabled(Long tenantId) configures whether or not respectively the full and/or the differential synchronization is enabled.
protected boolean isFullSyncEnabled(Long tenantId) { return true; } protected boolean isDifferentialSyncEnabled(Long tenantId) { return false; }
Note that the tenantId is passed here. In a non-multitenant setup, this parameter can simply be ignored. All methods of this superclass have the tenantId parameter. In a multi-tenant setup, one should write logic to loop over all the tenants in the system and call the sync methods for each of the tenants separately.
The following two methods will configure when the synchronizations will be scheduled (and executed asynchronously). The return value of these methods should be a (Spring-compatible) cron expression. Note that this typically will be configured in a configuration properties file rather than hardcoded. When nulll is returned, that particular synchronization won’t be scheduled.
protected String getScheduledFullSyncCronExpression() { return "0 0 0 * * ?"; // midnight } protected String getScheduledDifferentialSyncCronExpression() { return null; }
Now we get to the important part of the implementation: the actual fetching of users and groups. This is the method that is used during a full synchronization.
protected ExternalIdmQueryResult getAllUsersAndGroupsWithResolvedMembers(Long tenantId) { try { List<ExternalIdmUserImpl> users = readUsers(); List<ExternalIdmGroupImpl> groups = readGroups(users); return new ExternalIdmQueryResultImpl(users, groups); } catch (Exception e) { e.printStackTrace(); } return null; }
The return result, an instance of com.activiti.domain.sync.ExternalIdmQueryResult, which has a list of users in the form of com.activiti.domain.sync.ExternalIdmUser instances and a list of groups in the form of com.activiti.domain.sync.ExternalIdmGroup instances.
Note that each group has its members and child groups in it. Also note that these are all interfaces, so you are free to return any instance that implements these interfaces. By default there are simple POJO implementations of said interfaces: com.activiti.domain.sync.ExternalIdmQueryResultImpl, com.activiti.domain.sync.ExternalIdmUserImpl and com.activiti.domain.sync.ExternalIdmGroupImpl. These POJOs are also used in the example implementation above.
Important note: the ExternalIdmUser interface also defines a getPassword() method. Only return the actual password here if you want the user to authenticate against the default tables. The returned password will be securily hashed and stored that way. Return null if the authentication is done against an external system (LDAP is such an example). See further down to learn more about custom authentication.
The readUsers() and readGroups() methods will read the .txt mentioned above from the classpath and create instances of user and groups classes using the information in those files. For example:
protected List<ExternalIdmUserImpl> readUsers() throws IOException, ParseException { List<ExternalIdmUserImpl> users = new ArrayList<ExternalIdmUserImpl>(); InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("users.txt"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line = bufferedReader.readLine(); while (line != null) { String[] parsedLine = line.split(";"); ExternalIdmUserImpl user = new ExternalIdmUserImpl(); user.setId(parsedLine[0]); user.setOriginalSrcId(parsedLine[0]); user.setFirstName(parsedLine[1]); user.setLastName(parsedLine[2]); user.setEmail(parsedLine[3]); user.setPassword(parsedLine[4]); user.setLastModifiedTimeStamp(dateFormat.parse(parsedLine[5])); users.add(user); line = bufferedReader.readLine(); } inputStream.close(); return users; } protected List<ExternalIdmGroupImpl> readGroups(List<ExternalIdmUserImpl> users) throws IOException, ParseException { List<ExternalIdmGroupImpl> groups = new ArrayList<ExternalIdmGroupImpl>(); InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("groups.txt"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line = bufferedReader.readLine(); while (line != null) { String[] parsedLine = line.split(":"); String groupId = parsedLine[0]; ExternalIdmGroupImpl group = new ExternalIdmGroupImpl(); group.setOriginalSrcId(groupId); group.setName(groupId); List<ExternalIdmUserImpl> members = new ArrayList<ExternalIdmUserImpl>(); String[] memberIds = parsedLine[1].split(";"); for (String memberId : memberIds) { for (ExternalIdmUserImpl user : users) { if (user.getId().equals(memberId)) { members.add(user); } } } group.setUsers(members); group.setLastModifiedTimeStamp(dateFormat.parse(parsedLine[2])); groups.add(group); line = bufferedReader.readLine(); } inputStream.close(); return groups; }
For the differential synchronization a similar implementation could be made. Note that now a timestamp is passed, which indicates that the method should only return user/groups that are changed since that timestamp.
protected List<? extends ExternalIdmUser> getUsersModifiedSince(Date latestSyncDate, Long tenantId) { ... } protected List<? extends ExternalIdmGroup> getGroupsModifiedSince(Date latestSyncDate, Long tenantId) { .... }
The last two methods we need to implement are to indicate which users should become a tenant admin (or a tenant manager in a multi-tenant setup). This method should return an array of string with the id used in the external IDM store. More specifically, the strings in this array will be compared with the value in the ExternalIdmUser.getOriginalSrcId() method. Note that in practice these strings often will come from a configuration file rather than being hardcoded.
protected String[] getTenantManagerIdentifiers(Long tenantId) { return null; // No tenant manager } protected String[] getTenantAdminIdentifiers(Long tenantId) { return new String[] { "jlennon" }; }
That’s all there is to it. As shown, no actual synchronization logic needs to be written when extending from the AbstractExternalIdmSourceSyncService class. The implementation should only worry about configuration and the actual fetching of the user and group information.
On a first boot, all users/groups must sync for the first time, otherwise nobody would be able to log in. The LDAP synchronization logic does this automatically. When creating a custom synchronization service, a custom BootstrapConfigurer can be used to do the same thing:
package com.activiti.extension.bean; @Component public class MyBootstrapConfigurer implements BootstrapConfigurer { @Autowired private FileSyncService fileSyncService; public void applicationContextInitialized(org.springframework.context.ApplicationContext applicationContext) { fileSyncService.asyncExecuteFullSynchronizationIfNeeded(null); } }
This implements the com.activiti.api.boot.BootstrapConfigurer interface. If there is an instance implementing this interface on the classpath, it will be called when the application is booting up (more precisely: after the Spring application context has been initialized). Here, the class we created in the previous section, FileSyncService is injected. Note we add it to the component scanned package again and added the @component identifier.
Call the asyncExecuteFullSynchronizationIfNeeded() method. The null parameter means the default tenant (that is, this is a non-multitenant setup). This is a method from the com.activiti.api.idm.ExternalIdmSourceSyncService interface, which will do a full sync if no initial synchronization was done before.
As a side note, all synchronization logs are stored in a table IDM_SYNC_LOG in the database.
When a synchronization is executed, a log is kept. This log contains all information about the synchronization: users/groups that are created, updates of existing users/groups, membership additions/deletions and so on.
To access the log entries, an HTTP REST call can be done:
GET /api/enterprise/idm-sync-log-entries
Which returns a result like this (only an initial synchronization happened here):
[{"id":1,"type":"initial-ldap-sync","timeStamp":"2015-10-16T22:00:00.000+0000"}]
This call uses the following url parameters:
tenantId: Defaults to the tenantId of the users
start and size: Used for getting paged results back instead of one (potentially large) list.
Note that this call can only be done by a tenant administrator, or tenant manager in a multi-tenant setup.
We can now get the detailed log for each sync log entry, by taking an id from the previous response:
GET /api/enterprise/idm-sync-log-entries/{id}/logfile
This returns a .log file that contains for our example implementation
created-user: created user John Lennon (email=john.lennon@thebeatles.com) (dn=jlennon) added-capability: added capability tenant-mgmt to user jlennon created-user: created user Ringo Starr (email=ringo.starr@thebeatles.com) (dn=rstarr) created-user: created user George Harrison (email=george.harrison@beatles.com) (dn=gharrison) created-user: created user Paul McCartney (email=paul.mccartney@beatles.com) (dn=pmccartney) created-group: created group beatles added-user-to-group: created group membership of user jlennon for group beatles added-user-to-group: created group membership of user rstarr for group beatles added-user-to-group: created group membership of user gharrison for group beatles added-user-to-group: created group membership of user pmccartney for group beatles created-group: created group singers added-user-to-group: created group membership of user jlennon for group singers added-user-to-group: created group membership of user pmccartney for group singers
See Global security override [58] for more information on how to use the users.txt file as an authentication mechanism.
Configure security with the com.activiti.conf.SecurityConfiguration class. It allows you to switch between database and LDAP/Active Directory authentication out of the box. It also configures REST endpoints under "/app" to be protected using a cookie-based approach with tokens and REST endpoints under "/api" to be protected by Basic Auth.
You can override these defaults, if the out-of-the-box options are not adequate for your environment. The following sections describe the different options.
httpSecurity.antMatcher("/api/**")
Global security override is the most important override. It allows you to replace the default authentication mechanism.
The interface to implement the global security override is called com.activiti.api.security.AlfrescoSecurityConfigOverride. It has one method configureGlobal which is called instead of the default logic. It sets up either database-backed or LDAP-backed authentication if an instance implementing this interface is found on the classpath.
Building further on the example in Example implementation [61], use the users.txt file, in combination with the FileSyncService, so that the application uses the user information in the file to execute authentication.
Spring Security (which is used as underlying framework for security) expects an implementation of the org.springframework.security.authentication.AuthenticationProvider to execute the actual authentication logic. What we have to do in the configureGlobal method is then instantiate our custom class:
package com.activiti.extension.bean; @Component public class MySecurityOverride implements AlfrescoSecurityConfigOverride { public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) { MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider(); myAuthenticationProvider.setUserDetailsService(userDetailsService); auth.authenticationProvider(myAuthenticationProvider); } }
Note how this example passed the default UserDetailsService to this authentication provider. This class is responsible for loading the user data (and its capabilities or authorities in Spring Security lingo) from the database tables. Since we synchronized the user data using the same source, we can just pass it to our custom class.
So the actual authentication is done in the MyAuthenticationProvider class here. In this simple example, we just have to compare the password value in the users.txt file for the user. To avoid having to do too much low-level Spring Security plumbing, we let the class extend from the org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider class.
public static class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { protected Map<String, String> userToPasswordMapping = new HashMap<String, String>(); protected UserDetailsService userDetailsService; public MyAuthenticationProvider() { // Read users.txt, and create a {userId, password} map try { InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("users.txt"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); String line = bufferedReader.readLine(); while (line != null) { String[] parsedLine = line.split(";"); userToPasswordMapping.put(parsedLine[0], parsedLine[4]); line = bufferedReader.readLine(); } inputStream.close(); } catch (Exception e) { e.printStackTrace(); } } protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // We simply compare the password in the token to the one in the users.txt file String presentedPassword = authentication.getCredentials().toString(); String actualPassword = userToPasswordMapping.get(userDetails.getUsername()); if (!StringUtils.equals(presentedPassword, actualPassword)) { throw new BadCredentialsException("Bad credentials"); } } protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // Here we simply defer the loading to the UserDetailsService that was passed to this instance UserDetails loadedUser = null; try { loadedUser = userDetailsService.loadUserByUsername(username); } catch (Exception e) { throw new AuthenticationServiceException(e.getMessage(), e); } return loadedUser; } }
There’s one last bit to configure. By default, the application is configured to log in using the email address. Set the following property to switch that to the externalId, meaning the id coming from the external IDM source (jlennon in the users.txt file for example):
security.authentication.use-externalid=true
Use the following property to configure case-sensitivity for logins:
security.authentication.casesensitive=true
Alternatively, you can override the AuthenticationProvider that is used (instead of overriding the configureGlobal) by implementing the com.activiti.api.security.AlfrescoAuthenticationProviderOverride interface.
You can change the default security configuration of the REST API endpoints by implementing the com.activiti.api.security.AlfrescoApiSecurityOverride interface. By default, the REST API endpoints use the Basic Authentication method.
Similarly, you can override the default cookie+token based security configuration with the regular REST endpoints (those used by the UI) by implementing the com.activiti.api.security.AlfrescoWebAppSecurityOverride interface.
httpSecurity.antMatcher("/api/**")
If the default com.activiti.security.UserDetailsService does not meet the requirement (although it should cover most use cases), you can override the implementation with the com.activiti.api.security.AlfrescoUserDetailsServiceOverride interface.
By default, Alfresco Process Services uses the org.springframework.security.crypto.password.StandardPasswordEncoder for encoding passwords in the database. Note that this is only relevant when using database-backed authentication (so does not hold LDAP/Active Directory). This is an encoder that uses SHA-256 with 1024 iterations and a random salt.
You can override the default setting by implementing the com.activiti.api.security.AlfrescoPasswordEncoderOverride interface.
Alfresco Process Services comes with a REST API. The REST API exposes the generic Process Engine operations. It also includes a dedicated set of REST API endpoints for features specific to Alfresco Process Services.
By default, CORS is not allowed to provide a high level of security. This can be alleviated by using web proxies which consolidate different domains and ports or by enabling CORS in Alfresco Process Services configuration. For more information, see Configuring CORS [71].
The REST API uses authorization rules to determine a user’s access control for a process instance or task.
If you are using OAuth 2 to authenticate users for SSO, see OAuth 2 SSO [72] for more information.
If you choose to use Impersonation, you can impersonate a user with an Admin account to authenticate and set a different user for authorization. To enable this, add the activiti-user and activiti-user-value-type request headers to the REST API. Where, activiti-user should be set to the required user account identifier and activiti-user-value-type to the user account identifier type. The header activiti-user-value-type can be one of the following values:
userIdType: User’s database ID
userEmailType: User’s Email address
userExternalIdType: User’s ID in an external authentication service such as LDAP or Active Directory
For example, in the external-form-example Web application, an Admin account is used for authentication and a different user account to implement authorization.
OAuth 2 SSO support in Alfresco Process Services introduces a new set of components that allow developers to leverage the Alfresco REST APIs using OAuth 2 authorization.
a standard-based authorization infrastructure to integrate applications and solutions using Alfresco Process Services REST APIs with other enterprise applications which use OAuth.
All these components are used in configuring the OAuth 2 client. For more information, see Configuring the OAuth 2 module [83].
You may use an OAuth 2 Authorization server of your choice but for applications involving Alfresco Content Services, it is recommended that you use the Alfresco OAuth 2 Authorization server. To know more about installing and configuring the Alfresco OAuth 2 Authorization server, see Configuring the Alfresco OAuth 2 Authorization server [84].
Note that OAuth 2 is an authorization system and not an identity management system. Although it eliminates the need for custom applications to login via the REST API, it still requires all users to have a profile in Alfresco Process Services with a user name that matches the user name of the OAuth 2 Authorization server. However, there is no need for the passwords to match. Passwords are only useful if you want to allow users to log in to the standard Alfresco Content Services application.
You can use LDAP sync or the Alfresco Content Services Security Extensions to have a single identity service for both the Alfresco Content Services profiles and the OAuth 2 Authorization server.
security.oauth2.authentication.enabled=true security.oauth2.client.clientId=alfresco security.oauth2.client.clientSecret=secret security.oauth2.client.checkToken=http://localhost:9191/oauth/check_token
Property | Description |
---|---|
security.oauth2.authentication.enabled | Enables or disables the OAuth 2 client. To enable the OAuth 2 client, set this property to true. To disable it, set this property to false. |
security.oauth2.client.clientId | Specifies the credentials used by the Alfresco Process Services OAuth 2 client to communicate with the OAuth 2 Authorization server. |
security.oauth2.client.clientSecret | Specifies the credentials used by the Alfresco Process Services OAuth 2 client to communicate with the OAuth 2 Authorization server. |
security.oauth2.client.checkToken | Configures the OAuth 2 Authorization to be used. It contains the authorization URL obtained from the Authorization server. |
security.oauth2.basicAuth.enabled | Enables or disables basic authentication when OAuth 2 is configured. The default value is false. |
As a developer, you can integrate the OAuth 2 flow, which starts with getting the authorization token. For browser, mobile, and other UI-based applications, this will usually be done using a login UI interface provided by the service to the user. For communication purpose, the server-side applications will use the client secret.
Authorization: Bearer <token>
For example, to call the rest API for the app-version, either use:
GET /activiti-app/api/enterprise/app-version HTTP/1.1 Host: activiti.example.com Authorization: Bearerd1c7dc0b-b1e1-4039-923e-55199473bd5b
$ curl -i -H "Authorization: Bearer d1c7dc0b-b1e1-4039-923e-55199473bd5b" http://localhost:8080/activiti-app/api/enterprise/app-version
When a REST request is made using the OAuth 2 header, Alfresco Process Services acts as the Resource Server of the OAuth 2 specification. Using the OAuth 2 module Alfresco Process Services attempts to validate the token against the OAuth 2 Authorization server. This is done using the URL specified in the security.oauth2.client.checkToken property of the activiti-app.properties file.
Here's an example of the HTTP call made by Alfresco Process Services OAuth 2 module to validate the token:
POST /introspect HTTP/1.1 Host: ${security.oauth2.client.checkToken} Accept: application/json Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW <- base 64 encoding ${security.oauth2.client.clientId}:${security.oauth2.client.clientSecret} token=CHECK_ACCESS_TOKEN
The Authorization server responds with a JSON object as specified in the Introspection Response of the OAuth 2 specification [85]. One of the properties of the object is the user name, which matches the user name found in the user database of Alfresco Process Services. This allows the process service to identify which registered user is the one associated to the REST request.
Spring security is used to call the OAuth 2 server and validate the token. Token validation is an area that has been standardised recently. For more information, see OAuth 2.0 Token Introspection [86]. Commercial OAuth 2 servers or services may not be yet compliant with the standard. For more information, seehttp://stackoverflow.com/questions/12296017 [87].
For non-standard validation approaches, you may use apiSecurityOverride of the security extensibility provided by Alfresco Process Services and override the com.activiti.security.oauth2.Oauth2RequestHeaderService class using @Component(value = "ActivitiOauth2RequestHeaderService").
The server loads the properties from the application.properties file in order of precedence. The properties defined in locations higher in the list override those defined in lower locations.
The properties file contains the following properties
Property | Description | Default Value |
---|---|---|
Server.port | Specifies the port on which the Authorization server runs. | 9191 |
zuul.routes.ecm.url | Specifies the end-point URL for Alfresco Cloud Services installation to use. | http://localhost:8080 |
zuul.routes.bpm.url | Specifies the end-point URL for Alfresco Process Services installation to use. | http://localhost:9999 |
zuul.routes.ecm.path | Specifies the default path for ECM requests. For example, http://localhost:9191/ecm/alfresco/api/-default-/public/alfresco/versions/1/people. | /ecm |
zuul.routes.bpm.path | Specifies the default path for the BPM requests. For example, http://localhost:9191/bpm/activiti-app/api/enterprise/app-version. | /bpm |
authentication.oauth.jwt | Enables or disables the use of JWT tokens. Set it to true to instruct the server to use JWT tokens. Set it to false to configure the server to use the proprietary Alfresco token. | false |
authentication.oauth.corsFilter=true | Enable (true) or disable (false) CORS requests. | false |
authentication.oauth.ecm | Enables (true) or disables (false) authentication against Alfresco Content Services. | true |
authentication.oauth.bpm | Enables (true) or disables (false) authentication against Alfresco Process Services. | true |
authentication.oauth.tokenValidityInSeconds | Specifies the token lifetime or the lifetime in seconds of the access token. | 604800 |
java -jar alfresco-oauth2-<version>.war
$ curl -i -H "Authorization: Bearer <access_token>" http://localhost:9191/management/health
{"status":"UP"}
Use this information to know how the different scenarios are supported.
Authorization code
http://tools.ietf.org/html/rfc6749#section-4.1The authorization code grant type is used to obtain both access tokens and refresh tokens. It is optimized for confidential clients, such as server side application. Since this is a redirection-based flow, the client must be capable of interacting with the resource owner's user-agent (typically, a web browser) and capable of receiving incoming requests (via redirection) from the Authorization server.
Authorization Request
Here's an example of the authorization request:
curl -XPOST -vu alfrescoapp:secret 'http://localhost:9191/authorize?response_type=code&client_id=alfrescoapp&state=xyz& redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
where:
Parameter | Description | Required? |
---|---|---|
response_type | This value must be set to code. | Required |
client_id | Specifies the client identifier. | Required |
redirect_uri | Specifies the redirection endpoint after authentication. | Required |
state | Specifies an opaque value used by the client to maintain state between the request and callback sent for preventing cross-site request forgery. | Optional |
Your OAuth 2 module initiates the flow by directing the resource owner's user-agent to the authorization endpoint.
The Authorization server authenticates the resource owner.
The Authorization server establishes whether the resource owner grants or denies the client's access request.
Assuming the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier.
Authorization response
HTTP/1.1 302 Found Location: http://example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
where:
Parameter | Description | Notes |
---|---|---|
code | Specifies the authorization code generated by the authorization server. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minute is RECOMMENDED. The client MUST NOT use the authorization code more than once. | Required |
state | Specifies if this parameter was present in the client authorization request. It specifies the exact value received from the client. | Required |
The client makes a request to the token endpoint in order to get the access_token:
curl -XPOST -vu alfrescoapp:secret http://localhost:9191/grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
where:
Parameter | Description | Notes |
---|---|---|
grant_type | This value must be set to authorization_code. | Required |
code | Specifies the authorization code received from the Authorization server. | Required |
redirect_uri | Specifies the redirection endpoint after authentication. | Required |
client_id | Specifies if the client is not authenticating with the Authorization server. | Required |
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" "example_parameter":"example_value" }
Implicit
The implicit grant type (http://tools.ietf.org/html/rfc6749#section-4.2 [88] ) is used to obtain access tokens (it does not support the issuance of refresh tokens) and is optimized for public clients known to operate a particular redirection URI. These clients are typically implemented in a browser using a scripting language such as JavaScript clients or mobile applications. This flow is recommended when storing client id and client secret is not recommended
Authorization request
curl -XPOST -vu alfrescoapp:secret 'http://localhost:9191//authorize?response_type=token& client_id=alfrescoapp&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb'
where:
Parameter | Description | Notes |
---|---|---|
response_type | This value MUST be set to token. | Required |
client_id | Specifies the client identifier. | Required |
redirect_uri | Specifies the redirection endpoint after authentication. | Optional |
scope | Specifies if the client is not authenticating with the Authorization server. | Optional |
state | Specifies an opaque value used by the client to maintain state between the request and callback sent for preventing cross-site request forgery. | Required |
HTTP/1.1 302 Found Location: http://example.com/cb#access_token=91202244-431f-444a-b053-7f50716f2012&state=xyz&token_type=bearer&expires_in=3600
where:
Parameter | Description | Notes |
---|---|---|
access_token | Specifies the access token issued by the Authorization server. | Required |
token_type | Specifies the type of token. | Required |
expires_in | Specifies the lifetime in seconds of the access token. | Recommended |
scope | Specifies if the client is not authenticating with the Authorization server. | Optional |
state | Specifies an opaque value used by the client to maintain state between the request and callback sent for preventing cross-site request forgery. | Recommended |
Resource owner password credentials
The resource owner password credentials grant type [89] is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application.
curl -XPOST -vu alfrescoapp:secret 'http://localhost:9191/oauth/token?username=admin&password=tiger&grant_type=password'
where:
Parameter | Description | Notes |
---|---|---|
grant_type | This value MUST be set to password. | Required |
username | Specifies the resource owner username. | Required |
password | Specifies the resource owner password. | Required |
scope | Specifies if the client is not authenticating with the Authorization server. | Optional |
{ "access_token":"821c99d4-2c9f-4990-b68d-18eacaff54b2", "token_type":"bearer" "refresh_token":"e6f8624f-213d-4343-a971-980e83f734be", "expires_in":1799, "scope":"read write" }
curl -XPOST -vu alfrescoapp:secret 'http://localhost:9191/oauth/token?grant_type=refresh_token&refresh_token=<refresh_token>'
where:
Parameter | Description | Notes |
---|---|---|
grant_type | This value Value MUST be set to refresh_token. | Required |
refresh_token | Specifies the refresh token issued to the client. | Required |
{ "access_token":"821c99d4-2c9f-4990-b68d-18eacaff54b2", "token_type":"bearer" "refresh_token":"e6f8624f-213d-4343-a971-980e83f734be", "expires_in":1799, "scope":"read write" }
curl -i -H "Authorization: Bearer <access_token>" http://localhost:9191/secure
Secure Hello!
Client credentials
External Token
As defined in the OAuth 2 specification [91], it is possible to define custom grant. You can override the generation of the token using the grant_type, external_auth. Additionally, you can submit the token and the refresh token. This grant type can be used in the scenario where the OAuth server is already present and you want to use the proxy part of this server.
authentication.oauth.client.accessTokenUri= http://AUTH_SERVER/oauth/token authentication.oauth.client.userAuthorizationUri=http://AUTH_SERVER/oauth/authorize authentication.oauth.client.clientId= YOUR_CLIENT authentication.oauth.client.clientSecret= YOUR_SECRET
Access Token Request
curl -XPOST -vu alfrescoapp:secret 'http://localhost:9191/oauth/token?username=admin&password=admin&access_token=YOUR_CUSTOM_TOKEN& refresh_token=YOUR_CUSTOM_REFRESH_TOKEN&grant_type=external_token'
where:
Parameter | Description | Notes |
---|---|---|
grant_type | This value MUST be set to external_token. | Required |
username | Specifies the resource owner username. | Required |
password | Specifies the resource owner password. | Required |
scope | Specifies if the client is not authenticating with the Authorization server. | Optional |
{ "access_token":"821c99d4-2c9f-4990-b68d-18eacaff54b2", "token_type":"bearer" "refresh_token":"e6f8624f-213d-4343-a971-980e83f734be", "expires_in":1799, "scope":"read write" }
Alfresco Process Services comes with a built-in REST API Explorer. This lets you discover and test the REST APIs of a locally running Process Services instance.
The REST API Explorer is based on the OpenAPI (Swagger) initiative [92] and provides an interface for the REST API. You can browse the available API endpoints and test operations available within a particular API group.
Access the REST API Explorer with the link: http://localhost:8080/activiti-app/api-explorer.html [93].
There is also a public REST API Explorer [94].
This screenshot shows what the REST API Explorer looks like:
Click on a link to view the available operations for a particular group of APIs.
For example, to explore the operations on a specific entity, Admin Tenants: Manage Tenants API, just click on it:
Click on an operation to test it against the locally running Process Services instance.
When you click Try it out!, you'll see the following response:
Alfresco Process Services provides a RAML file that works with popular REST API development tools.
The RAML file complements the REST API Explorer [66], providing a best-in-class enterprise tooling for APIs.
RESTful API Modeling Language (RAML) is a language to describe RESTful APIs. The language is YAML-based with a json format available, and it provides the constructs to describe RESTful or practically-RESTful APIs. Practically-RESTful APIs are those that do not comply with the all constraints of REST.
The language aims to promote reuse, discovery and pattern-sharing, as well as merit-based emergence of patterns. Tooling for RAML varies from modeling to software life cycle management and API description conversion. For more information about RAML, see https://raml.org [95].
Alfresco Process Services provides a description of all enterprise REST APIs using RAML and in json format. The description follows RAML 0.8 but can easily be converted to the recent RAML 1.0 standard by using tools like Apimatic ().
http(s)://<alfresco process services host>:port/activiti-app/raml/activiti.raml
This URL returns the entire RAML description of the enterprise APIs.
Using the RAML file for Alfresco Process Services
The Alfresco Process Services RAML file can be used with tools supporting RAML to integrate it in API life cycle of enterprise systems.
Mulesoft provides a free RAML IDE called API Workbench. This is a plugin for the free editor, Atom, that can be used to view the Alfresco Process Services RAML file. For information on how to download and setup the Atom plugin, see http://apiworkbench.com/docs [96].
In addition, Mulesoft provides a web-based RAML API designer that can be used to combine Alfresco Process Services REST APIs in RAML-based API and system design. See https://www.mulesoft.com/platform/api/anypoint-designer [97].
For a full list of tools that can use RAML throughout the entire application development life cycle see http://raml.org/projects/projects [98].
The REST API exposes data and operations that are specific to Alfresco Process Services.
In contrast to the Process Engine REST API, the Alfresco Process Services REST API can be called using any user. The following sections describe the supported REST API endpoints.
To retrieve information about the Process Services version, use the following command:
GET api/enterprise/app-version
Response:
{ "edition": "Alfresco Activiti Enterprise BPM Suite", "majorVersion": "1", "revisionVersion": "0", "minorVersion": "2", "type": "bpmSuite", }
This operation returns account information for the current user. This is useful to get the name, email, the groups that the user is part of, the user picture, and so on.
GET api/enterprise/profile
Response:
{ "tenantId": 1, "firstName": "John", "password": null, "type": "enterprise", "company": null, "externalId": null, "capabilities": null, "tenantPictureId": null, "created": "2015-01-08T13:22:36.198+0000", "pictureId": null, "latestSyncTimeStamp": null, "tenantName": "test", "lastName": "Doe", "id": 1000, "lastUpdate": "2015-01-08T13:34:22.273+0000", "email": "johndoe@alfresco.com", "status": "active", "fullname": "John Doe", "groups": [ { "capabilities": null, "name": "analytics-users", "tenantId": 1, "users": null, "id": 1, "groups": null, "externalId": null, "status": "active", "lastSyncTimeStamp": null, "type": 0, "parentGroupId": null }, { "capabilities": null, "name": "Engineering", "tenantId": 1, "users": null, "id": 2000, "groups": null, "externalId": null, "status": "active", "lastSyncTimeStamp": null, "type": 1, "parentGroupId": null }, { "capabilities": null, "name": "Marketing", "tenantId": 1, "users": null, "id": 2001, "groups": null, "externalId": null, "status": "active", "lastSyncTimeStamp": null, "type": 1, "parentGroupId": null } ] }
To update user information (first name, last name or email):
POST api/enterprise/profile
The body of the request should resemble the following text:
{ "firstName" : "John", "lastName" : "Doe", "email" : "john@alfresco.com", "company" : "Alfresco" }
To get the user picture, use following REST call:
GET api/enterprise/profile-picture
To change this picture, do an HTTP POST to the same URL, with the picture as multipart file in the body.
Finally, to change the password:
POST api/enterprise/profile-password
with a json body that looks like
{ "oldPassword" : "12345", "newPassword" : "6789" }
When a user logs into Process Services, the landing page is displayed containing all the apps that the user is allowed to see and use.
The corresponding REST API request to get this information is:
GET api/enterprise/runtime-app-definitions
Response:
{ "size": 3, "total": 3, "data": [ { "deploymentId": "26", "name": "HR processes", "icon": "glyphicon-cloud", "description": null, "theme": "theme-6", "modelId": 4, "id": 1 }, { "deploymentId": "2501", "name": "Sales onboarding", "icon": "glyphicon-asterisk", "description": "", "theme": "theme-1", "modelId": 1002, "id": 1000 }, { "deploymentId": "5001", "name": "Engineering app", "icon": "glyphicon-asterisk", "description": "", "theme": "theme-1", "modelId": 2001, "id": 2000 } ], "start": 0 }
The id and modelId property of the apps are important here, as they are used in various operations described below.
To retrieve all App definitions including ones that were not deployed at runtime:
GET api/enterprise/models?filter=myApps&modelType=3&sort=modifiedDesc
The request parameters
filter : Possible values: myApps, sharedWithMe, sharedWithOthers, favorite.
modelType : Must be 3 for App definition models.
sort : modifiedDesc, modifiedAsc, nameAsc, nameDesc (default modifiedDesc).
It is possible to export app definitions and import them again. From the REST API point of view, this is useful to bootstrap an environment (for users or continuous integration).
To export an app definition, you need the modelId from a runtime app or the id of an app definition model, and call.
GET api/enterprise/app-definitions/{modelId}/export
This will return a zip file containing the app definition model and all related models (process definitions and forms).
To import an app again, post the zip file as multipart file to
POST api/enterprise/app-definitions/import
To import an app to an existing app definition to create a new version instead of importing a new app definition, post the zip file as multipart file to
POST api/enterprise/app-definitions/{modelId}/import
Before an app model can be used, it needs to be published. This can be done through following call:
POST api/enterprise/app-definitions/{modelId}/publish
A JSON body is required for the call. You can either use an empty one or the following example:
{ "comment": "", "force": false }
To add it to your landing page, deploy the published app:
POST api/enterprise/runtime-app-definitions
Where, appDefinitions is an array of IDs, for example:
{ "appDefinitions" : [{"id" : 1}, {"id" : 2}] }
To retrieve a list of process definition models:
GET api/enterprise/models?filter=myprocesses&modelType=0&sort=modifiedDesc
The request parameters
filter : Possible values: myprocesses, sharedWithMe, sharedWithOthers, favorite.
modelType : Must be 0 for process definition models.
sort : Possible values: modifiedDesc, modifiedAsc, nameAsc, nameDesc (default modifiedDesc).
Both app definition and process definition models are versioned.
To retrieve details about a particular model (process, form, decision rule or app):
GET api/enterprise/models/{modelId}
Example response:
{ "createdBy": 1, "lastUpdatedBy": 1, "lastUpdatedByFullName": " Administrator", "name": "aad", "id": 2002, "referenceId": null, "favorite": false, "modelType": 0, "comment": "", "version": 3, "lastUpdated": "2015-01-10T16:24:27.893+0000", "stencilSet": 0, "description": "", "createdByFullName": " Administrator", "permission": "write", "latestVersion": true }
The response shows the current version of the model.
To retrieve a thumbnail of the model:
GET api/enterprise/models/{modelId}/thumbnail
To get the version information for a model:
GET api/enterprise/models/{modelId}/history
Example response:
{ "size": 2, "total": 2, "data": [ { "createdBy": 1, "lastUpdatedBy": 1, "lastUpdatedByFullName": " Administrator", "name": "aad", "id": 3000, "referenceId": null, "favorite": null, "modelType": 0, "comment": "", "version": 2, "lastUpdated": "2015-01-10T16:15:50.579+0000", "stencilSet": 0, "description": "", "createdByFullName": " Administrator", "permission": null, "latestVersion": false }, { "createdBy": 1, "lastUpdatedBy": 1, "lastUpdatedByFullName": " Administrator", "name": "aad", "id": 2000, "referenceId": null, "favorite": null, "modelType": 0, "comment": null, "version": 1, "lastUpdated": "2015-01-10T16:07:41.831+0000", "stencilSet": 0, "description": "", "createdByFullName": " Administrator", "permission": null, "latestVersion": false } ], "start": 0 }
To get a particular older version:
GET api/enterprise/models/{modelId}/history/{modelHistoryId}
To create a new model:
POST api/enterprise/models/
with a json body that looks like:
{ "modelType": 0, "name": "My process", "description": "This is my favourite process!" }
The modelType property defines the kind of model that is created:
0 is a BPMN 2.0 process model
1 is a step process model
2 is a form model
3 is an app model
4 is a decision table model
Following properties are optional:
stencilSet : the identifier of the stencilset in case a non-default stencilset needs to be used.
To update the details of a model:
PUT api/enterprise/models/{modelId}
with a json body that looks like:
{ "name": "New name", "description": "New description" }
To favorite a model:
PUT api/enterprise/models/{modelId}
with as json body:
{ "favorite": true }
To delete a model:
DELETE api/enterprise/models/{modelId}
To duplicate a model:
POST api/enterprise/models/{modelId}/clone
with as json body:
{ "name": "Cloned model" }
To convert a step process to a BPMN 2.0 process, add "modelType" : 0 to the body.
To export a process definition model to a BPMN 2.0 XML file:
GET api/enterprise/models/{processModelId}/bpmn20
For a previous version of the model:
GET api/enterprise/models/{processModelId}/history/{processModelHistoryId}/bpmn20
To import a BPMN 2.0 xml file:
POST api/enterprise/process-models/import
With the BPMN 2.0 XML file in the body as a multipart file and the file as value for the file property.
Get a list of process definitions (visible within the tenant of the user):
GET api/enterprise/process-definitions
Example response:
{ "size": 5, "total": 5, "data": [ { "id": "demoprocess:1:7504", "name": "Demo process", "description": null, "key": "demoprocess", "category": "http://www.activiti.org/test", "version": 1, "deploymentId": "7501", "tenantId": "tenant_1", "hasStartForm": true }, ... ], "start": 0 }
Following parameters are available:
latest: A boolean value, indicating that only the latest versions of process definitions must be returned.
appDefinitionId: Returns process definitions that belong to a certain app.
To get the candidate starters associated to a process definition:
GET api/enterprise/process-definitions/{processDefinitionId}/identitylinks/{family}/{identityId}
Where:
processDefinitionId: The ID of the process definition to get the identity links for.
family: Indicates groups or users, depending on the type of identity link.
identityId: The ID of the identity.
To add a candidate starter to a process definition:
POST api/enterprise/process-definitions/{processDefinitionId}/identitylinks
Request body (user):
{ "user" : "1" }
Request body (group):
{ "group" : "1001" }
To delete a candidate starter from a process definition:
DELETE api/enterprise/process-definitions/{processDefinitionId}/identitylinks/{family}/{identityId}
When process definition has a start form (hasStartForm is true as in the call above), the start form can be retrieved as follows:
GET api/enterprise/process-definitions/{process-definition-id}/start-form
Example response:
{ "processDefinitionId": "p1:2:2504", "processDefinitionName": "p1", "processDefinitionKey": "p1", "fields": [ { "fieldType": "ContainerRepresentation", "id": "container1", "name": null, "type": "container", "value": null, "required": false, "readOnly": false, "overrideId": false, "placeholder": null, "optionType": null, "hasEmptyValue": null, "options": null, "restUrl": null, "restIdProperty": null, "restLabelProperty": null, "layout": null, "sizeX": 0, "sizeY": 0, "row": 0, "col": 0, "visibilityCondition": null, "fields": { "1": [ { "fieldType": "FormFieldRepresentation", "id": "label1", "name": "Label1", "type": "text", "value": null, "required": false, "readOnly": false, "overrideId": false, "placeholder": null, "optionType": null, "hasEmptyValue": null, "options": null, "restUrl": null, "restIdProperty": null, "restLabelProperty": null, "layout": { "row": 0, "column": 0, "colspan": 1 }, "sizeX": 1, "sizeY": 1, "row": 0, "col": 0, "visibilityCondition": null } ], "2": [ ] } }, { "fieldType": "DynamicTableRepresentation", "id": "label21", "name": "Label 21", "type": "dynamic-table", "value": null, "required": false, "readOnly": false, "overrideId": false, "placeholder": null, "optionType": null, "hasEmptyValue": null, "options": null, "restUrl": null, "restIdProperty": null, "restLabelProperty": null, "layout": { "row": 10, "column": 0, "colspan": 2 }, "sizeX": 2, "sizeY": 2, "row": 10, "col": 0, "visibilityCondition": null, "columnDefinitions": [ { "id": "p2", "name": "c2", "type": "String", "value": null, "optionType": null, "options": null, "restUrl": null, "restIdProperty": null, "restLabelProperty": null, "required": true, "editable": true, "sortable": true, "visible": true } ] } ], "outcomes": [ ] }
Note: To retrieve field values such as the typeahead field, use the following REST endpoint:
GET api/enterprise/process-definitions/{processDefinitionId}/start-form-values/{field}
This returns a list of form values.
To start process instances, use:
POST api/enterprise/process-instances
With a json body that contains following properties:
processDefinitionId: The process definition identifier. Do not use it with processDefinitionKey.
processDefinitionKey: The process definition key. Do not use it with processDefinitionId.
name: The name to give to the created process instance.
values: A JSON object with the form field Id and form field values. The Id of the form field is retrieved from the start form call (see above).
outcome: If the start form has outcomes, this is one of those values.
variables: Contains a JSON array of variables. Values and outcomes can’t be used with variables.
The response will contain the process instance details including the ID.
Once started, the completed form (if defined) can be fetched using:
GET /enterprise/process-instances/{processInstanceId}/start-form
To get the list of process instances:
POST api/enterprise/process-instances/query
with a json body containing the query parameters. The following parameters are possible:
processDefinitionId
appDefinitionId
state (possible values are running, completed and all)
sort (possible values are created-desc, created-asc, ended-desc, ended-asc)
start (for paging, default 0)
size (for paging, default 25)
Example response:
{ "size": 6, "total": 6, "start": 0, "data":[ {"id": "2511", "name": "Test step - January 8th 2015", "businessKey": null, "processDefinitionId": "teststep:3:29"...}, ... ] }
To get a process instance:
GET api/enterprise/process-instances/{processInstanceId}
To get diagram for a process instance:
GET api/enterprise/process-instances/{processInstanceId}/diagram
To delete a Process Instance:
DELETE api/enterprise/process-instances/{processInstanceId}
To suspend a process instance:
PUT api/enterprise/process-instances/{processInstanceId}/suspend
To activate a process instance:
PUT api/enterprise/process-instances/{processInstanceId}/activate
Where, processinstanceId is the Id of the process instance.
GET api/enterprise/process-instances/{processInstanceId}
DELETE api/enterprise/process-instances/{processInstanceId}
If you need the audit log information as a JSON you can use the next URL:
GET api/enterprise/process-instances/{process-instance-id}/audit-log
Response
200 Ok
Returns a JSON string representing the full audit log for the requested process instance. For example:
{ "processInstanceId": "5", "processInstanceName": "myProcessInstance", "processDefinitionName": "TEST decision process", "processDefinitionVersion": "1", "processInstanceStartTime": "Wed Jan 20 16:18:46 EET 2016", "processInstanceEndTime": null, "processInstanceInitiator": "Mr Activiti", "entries": [ { "index": 1, "type": "startForm", "timestamp": "Wed Jan 20 16:18:46 EET 2016", "selectedOutcome": null, "formData": [ { "fieldName": "Text1", "fieldId": "text1", "value": "TEST" } ], "taskName": null, "taskAssignee": null, "activityId": null, "activityName": null, "activityType": null "startTime": "Thu Feb 16 16:32:05 GMT 2017", "endTime": "Thu Feb 16 16:32:05 GMT 2017", "durationInMillis": 1 }, { "index": 2, "type": "activityExecuted", "timestamp": "Wed Jan 20 16:18:46 EET 2016", "selectedOutcome": null, "formData": [], "taskName": null, "taskAssignee": null, "activityId": "startEvent1", "activityName": "", "activityType": "startEvent" "startTime": "Thu Feb 16 16:32:05 GMT 2017", "endTime": "Thu Feb 16 16:32:09 GMT 2017", "durationInMillis": 24054 }, { "index": 3, "type": "activityExecuted", "timestamp": "Wed Jan 20 16:18:47 EET 2016", "selectedOutcome": null, "formData": [], "taskName": null, "taskAssignee": null, "activityId": "sid-15E18ED8-252F-4A24-9E93-68F53FE28535", "activityName": "", "activityType": "serviceTask" "startTime": "Thu Feb 16 16:32:05 GMT 2017", "endTime": "Thu Feb 16 16:32:09 GMT 2017", "durationInMillis": 24054 }, { "index": 4, "type": "activityExecuted", "timestamp": "Wed Jan 20 16:18:48 EET 2016", "selectedOutcome": null, "formData": [], "taskName": null, "taskAssignee": null, "activityId": "sid-001FD811-C171-40E3-9C62-602621672022", "activityName": "", "activityType": "userTask" "startTime": "Thu Feb 16 16:32:05 GMT 2017", "endTime": "Thu Feb 16 16:32:09 GMT 2017", "durationInMillis": 24054 }, { "index": 5, "type": "taskCreated", "timestamp": "Wed Jan 20 16:18:48 EET 2016", "selectedOutcome": null, "formData": [], "taskName": null, "taskAssignee": "Mr Activiti", "activityId": null, "activityName": null, "activityType": null "startTime": "Thu Feb 16 16:32:05 GMT 2017", "endTime": "Thu Feb 16 16:32:09 GMT 2017", "durationInMillis": 24054 } ], "decisionInfo": { "calculatedValues": [ { "name": "outputVariable1", "value": "1.0" } ], "appliedRules": [ { "title": "Rule 1 (TEST Decision Table 1)", "expressions": [ { "type": "CONDITION", "variable": "text1", "value": "== 'TEST'" }, { "type": "OUTCOME", "variable": "outputVariable1", "value": "1" } ] } ] } }
A process instance can have several variables.
To get process instance variables:
GET api/enterprise/process-instances/{processInstanceId}/variables
Where, processInstanceId is the Id of the process instance.
To create process instance variables:
POST api/enterprise/process-instances/{processInstanceId}/variables
To update existing variables in a process instance:
PUT api/enterprise/process-instances/{processInstanceId}/variables
Example response:
{ "name":"myVariable", "type":"string", "value":"myValue" }
Where:
name - Name of the variable
type - Type of variable, such as string
value - Value of the variable
To update a single variable in a process instance:
PUT api/enterprise/process-instances/{processInstanceId}/variables/{variableName}
To get a single variable in a process instance:
GET api/enterprise/process-instances/{processInstanceId}/variables/{variableName}
To get all process instance variables:
GET api/enterprise/process-instances/{processInstanceId}/variables
To get a specific process instance variable:
GET api/enterprise/process-instances/{processInstanceId}/variables/{variableName}
To delete a specific process instance variable:
DELETE api/enterprise/process-instances/{processInstanceId}/variables/{variableName}
Either the users or groups involved with a process instance.
To create an identity link of a process instance:
POST api/enterprise/process-instances/{processInstanceId}/identitylinks
Example request:
{ "user": "1", "type": "customType" }
Get identity links of a process instance:
GET api/enterprise/process-instances/{processInstanceId}/identitylinks
Get identity links by family type of a process instance:
GET api/enterprise/process-instances/{processInstanceId}/identitylinks/{family}
Where, Family should contain users or groups, depending on the identity you want to link.
To get involved people in a process instance:
GET api/enterprise/process-instances/{processInstanceId}/identitylinks
You can get identity links for either user or groups. For example:
GET api/enterprise/process-instances/{processInstanceId}/identitylinks/users GET api/enterprise/process-instances/{processInstanceId}/identitylinks/groups
POST api/enterprise/tasks/query
which includes a JSON body containing the query parameters.
The following parameters are available:
appDefinitionId
processInstanceId
processDefinitionId
text (the task name will be filtered with this, using like semantics : %text%)
assignment
assignee : where the current user is the assignee
candidate: where the current user is a task candidate
group_x: where the task is assigned to a group where the current user is a member of. The groups can be fetched through the profile REST endpoint
no value: where the current user is involved
state (completed or active)
includeProcessVariables (set to true to include process variables in the response)
sort (possible values are created-desc, created-asc, due-desc, due-asc)
start (for paging, default 0)
size (for paging, default 25)
Example response:
{ "size": 6, "total": 6, "start": 0, "data":[ { "id": "2524", "name": "Task", "description": null, "category": null, "assignee":{"id": 1, "firstName": null, "lastName": "Administrator", "email": "admin@app.activiti.com"}, "created": "2015-01-08T10:58:37.193+0000", "dueDate": null, "endDate": null, "duration": null, "priority": 50, "processInstanceId": "2511", "processDefinitionId": "teststep:3:29", "processDefinitionName": "Test step", "processDefinitionDescription": null, "processDefinitionKey": "teststep", "processDefinitionCategory": "http://www.activiti.org/test", "processDefinitionVersion": 3, "processDefinitionDeploymentId": "26", "formKey": "5" } ... ] }
GET api/enterprise/tasks/{taskId}
Response is similar to the list response.
GET api/enterprise/task-forms/{taskId}
The response is similar to the response from the Start Form.
To retrieve Form field values that are populated through a REST back-end:
GET api/enterprise/task-forms/{taskId}/form-values/{field}
Which returns a list of form field values
To complete a Task form:
POST api/enterprise/task-forms/{taskId}
with a json body that contains:
values: A json object with the form field ID - form field values. The Id of the form field is retrieved from the start form call (see above).
outcome: Retrieves outcome values if defined in the Start form.
To save a Task form:
POST api/enterprise/task-forms/{taskid}/save-form
Example response:
{ "values": {"formtextfield":"snicker doodle"}, "numberfield":"6", "radiobutton":"red" }
Where the json body contains:
values : A json object with the form field ID - form field values. The Id of the form field is retrieved from the Start Form call (see above).
To retrieve a list of variables associated with a Task form:
GET api/enterprise/task-forms/{taskid}/variables
Example response
[ { "id": "initiator", "type": "string", "value": "3205" }, { "id": "FormField2", "type": "string", "value": "TestVariable2" }, { "id": "FormField1", "type": "string", "value": "TestVariable1" } ]
To create a task (for the user in the authentication credentials) that is not associated with a process instance:
POST api/enterprise/tasks
with a json body that contains the following properties:
name
description
To update the details of a task:
PUT api/enterprise/tasks/{taskId}
with a json body that can contain name, description and dueDate (ISO 8601 string)
For example:
Example request:
{ "name" : "IchangedTaskName", "description" : "description-updated", "dueDate" : "2015-01-11T22:59:59.000Z", "priority":10, "formKey": "100" }
To delegate a task:
PUT api/enterprise/tasks/{taskId}/action/delegate
Example request:
{ "userId": "1000" }
To resolve a task:
PUT api/enterprise/tasks/{taskId}/action/resolve
To complete a task (standalone or without a task form) (Note: No json body needed!):
PUT api/enterprise/tasks/{taskId}/action/complete
To claim a task (in case the task is assigned to a group):
PUT api/enterprise/tasks/{taskId}/action/claim
No json body needed. The task will be claimed by the user in the authentication credentials.
To assign a task to a user:
PUT api/enterprise/tasks/{taskId}/action/assign
with a json body that contains the assignee property set to the ID of a user.
To involve a user with a task:
PUT api/enterprise/tasks/{taskId}/action/involve
with a json body that contains the userId property set to the ID of a user.
To remove an involved user from a task:
PUT api/enterprise/tasks/{taskId}/action/remove-involved
with a json body that contains the userId property set to the ID of a user.
To attach a form to a task:
PUT api/enterprise/tasks/{taskId}/action/attach-form
with a json body that contains the formId property set to the the ID of a form.
To attach a form to a task:
DELETE api/enterprise/tasks/{taskId}/action/remove-form
To create new task variables:
POST api/enterprise/tasks/{taskId}/variables
To get all task variables:
GET api/enterprise/tasks/{taskId}/variables
To get a task variable by name:
GET api/enterprise/tasks/{taskId}/variables/{variableName}
To update an existing task variable:
PUT api/enterprise/tasks/{taskId}/variables/{variableName}
Example response:
{ "name":"myVariable", "scope":"local", "type":"string", "value":"myValue" }
Where:
name - Name of the variable.
scope - Global or local. If global is provided, then the variable will be a process-instance variable.
type - Type of variable, such as string.
value - Value of the variable.
To delete a task variable:
DELETE api/enterprise/tasks/{taskId}/variables/{variableName}
To delete all task variables:
DELETE api/enterprise/tasks/{taskId}/variables
Where, taskId is the ID of the task.
To get all identity links for a task:
GET api/enterprise/tasks/{taskId}/identitylinks
To create an identity link on a task:
POST api/enterprise/tasks/{taskId}/identitylinks
Example response:
{ "user": "1", "type": "customType" }
To get a single identity link on a task:
GET api/enterprise/tasks/{taskId}/identitylinks/{family}/{identityId}/{type}
To delete an identity link on a task:
DELETE api/enterprise/tasks/{taskId}/identitylinks/{family}/{identityId}/{type}
Where:
taskId: The ID of the task.
family: Indicates either groups or users, depending on the type of identity.
identityId: The ID of the identity.
type: The type of identity link.
Custom task queries can be saved as a user task filter. To get the list of task filters for the authenticated user:
GET api/enterprise/filters/tasks
with an option request parameter appId to limit the results to a specific app.
To get a specific user task filter:
GET api/enterprise/filters/tasks/{userFilterId}
To create a new user task filter:
POST api/enterprise/filters/tasks
with a json body that contains following properties:
name : Name of the filter.
appId : App ID where the filter can be used.
icon : Path of the icon image.
filter
sort : Possible values: created-desc, created-asc, due-desc, due-asc.
state : Open, completed.
assignment : Involved, assignee, or candidate.
To update a user task filter:
PUT api/enterprise/filters/tasks/{userFilterId}
with a json body that contains following properties:
name : Name of the filter
appId : App ID where the filter can be used.
icon : Path of the icon image.
filter
sort : Created-desc, created-asc, due-desc, due-asc.
state : Open, completed.
assignment : Involved, assignee, or candidate
To delete a user task filter:
DELETE api/enterprise/filters/tasks/{userFilterId}
To order the list of user task filters:
PUT api/enterprise/filters/tasks
with a json body that contains following properties:
order : Array of user task filter IDs.
appId : App ID.
To get a list of user process instance filters
GET api/enterprise/filters/processes
with an option request parameter appId to limit the results to a specific app.
To get a specific user process instance task filter:
GET api/enterprise/filters/processes/{userFilterId}
To create a user process instance task filter:
PUT api/enterprise/filters/processes
with a json body that contains following properties:
name : Name of the filter.
appId : App ID where the filter can be used.
icon : Path of the icon image.
filter
sort : Created-desc, created-asc.
state : Running, completed, or all.
To update a user process instance task filter:
PUT api/enterprise/filters/processes/{userFilterId}
with a json body that contains following properties:
name : Name of the filter.
appId : App ID, where the filter can be used.
icon : Path of the icon image.
filter
sort : Possible values: created-desc, created-asc.
state : Running, completed, or all.
To delete a user process instance task filter
DELETE api/enterprise/filters/processes/{userFilterId}
Comments can be added to a process instance or a task.
To get the list of comments:
GET api/enterprise/process-instances/{processInstanceId}/comments
GET api/enterprise/tasks/{taskId}/comments
To create a comments:
POST api/enterprise/process-instances/{processInstanceId}/comments
POST api/enterprise/tasks/{taskId}/comments
with in the json body one property called message, with a value that is the comment text.
You can add checklists to a task for tracking purposes.
To get a checklist:
GET api/enterprise/tasks/{taskId}/checklist
To create a checklist:
POST api/enterprise/tasks/{taskId}/checklist
Example request body:
{ "assignee": {"id": 1001}, "name": "mySubtask", "parentTaskId": "20086" }
To change the order of the items on a checklist:
PUT api/enterprise/tasks/{taskId}/checklist
with a json body that contains an ordered list of checklist items ids:
order : Array of checklist item ids
To obtain the audit information for a specific task in JSON format, use the following URL:
GET api/enterprise/tasks/{taskId}/audit
Response
200 Ok
If everything works as expected and the task is accessible to the current user, then the response will be as follows:
{ "taskId": "18", "taskName": null, "processInstanceId": "5", "processDefinitionName": "TEST decision process", "processDefinitionVersion": 1, "assignee": "Mr Activiti", "startTime": "Wed Jan 20 22:03:05 EET 2016", "endTime": "Wed Jan 20 22:03:09 EET 2016", "formData": [], "selectedOutcome": null, "comments": [] }
The Process Engine REST API is a supported equivalent of the Activiti Open Source API. This means that all operations described in the Activiti User Guide [128] are available as documented there, except for REST endpoints that are not relevant for the enterprise product (for example, forms, as they are implemented differently).
This REST API is available on <your-server-and-context-root>/api/
<your-server-and-context-root>/api/repository/process-definitions
For example <your-server-and-context-root>/api/repository/process-definitions?tenantId=tenant_1
This section covers the examples for querying historic process instances and task instances in the Alfresco Process Services API. You can query for historic process instances and tasks to get information about ongoing and past process instances, or tasks.
To run a historic process instance query:
POST api/enterprise/historic-process-instances/query
To run a historic task instance query:
POST api/enterprise/historic-tasks/query
The following table lists the request parameters to be used in the JSON body POST. For example, to filter historic process instances that completed before the given date (startedBefore):
POST api/enterprise/historic-process-instances/query
With a JSON body request:
{ "startedBefore":"2016-06-16", }
Example response:
{ "size": 25, "total": 200, "start": 0, "data": [ { "id": "2596", "name": "Date format example - June 7th 2016", "businessKey": null, "processDefinitionId": "dateformatexample:1:2588", "tenantId": "tenant_1", "started": "2016-06-07T14:18:34.433+0000", "ended": null, "startedBy": { "id": 1, "firstName": null, "lastName": "Administrator", "email": "admin@app.activiti.com" }, { "id": "2596", . . .
Where, *size is the size of the page or number of items per page. By default, the value is 25. * start is the page to start on. Pages are counted from 0-N. By default, the value is 0, which means 0 will be the first page.
processInstanceId |
An ID of the historic process instance. |
processDefinitionKey |
The process definition key of the historic process instance. |
processDefinitionId |
The process definition id of the historic process instance. |
businessKey |
The business key of the historic process instance. |
involvedUser |
An involved user of the historic process instance. Where, InvolvedUser is the ID of the user. |
finished |
Indicates if the historic process instance is complete. Where, the value may only be True, as the default values are True or False. |
superProcessInstanceId |
An optional parent process id of the historic process instance. |
excludeSubprocesses |
Returns only historic process instances which aren’t sub-processes. |
finishedAfter |
Returns historic process instances that finished after the given date. The date is displayed in yyyy-MM-ddTHH:MM:SS format. |
finishedBefore |
Returns historic process instances that finished before the given date. The date is displayed in yyyy-MM-ddTHH:MM:SS format. |
startedAfter |
Returns historic process instances that were started after the given date. The date is displayed in yyyy-MM-ddTHH:MM:SS format. |
startedBefore |
Returns historic process instances that were started before the given date. The date is displayed in yyyy-MM-ddTHH:MM:SS format. |
startedBy |
Returns only historic process instances that were started by the selected user. |
includeProcessVariables |
Indicates if the historic process instance variables should be returned. |
tenantId |
Returns instances with the given tenantId. |
tenantIdLike |
Returns instances with a tenantId like the given value. |
withoutTenantId |
If true, only returns instances without a tenantId set. If false, the withoutTenantId parameter is ignored. |
The following table lists the request parameters that can be used in the JSON body POST. For example, in case of taskCompletedAfter:
---- POST api/enterprise/historic-tasks/query ----
With a json body request: { "taskCompletedAfter":"2016-06-16", "size":50, "start":0 }
Example response:
{ "size": 4, "total": 4, "start": 0, "data": [ { "id": "7507", "name": "my task", "assignee": { "id": 1000, "firstName": "Homer", "lastName": "Simpson", "email": "homer.simpson@gmail.com" }, "created": "2016-06-17T15:14:26.938+0000", "dueDate": null, "endDate": "2016-06-17T16:09:39.197+0000", "duration": 3312259, "priority": 50, . . .
taskId |
An ID of the historic task instance. |
processInstanceId |
The process instance id of the historic task instance. |
processDefinitionKey |
The process definition key of the historic task instance. |
processDefinitionKeyLike |
The process definition key of the historic task instance, which matches the given value. |
processDefinitionId |
The process definition id of the historic task instance. |
processDefinitionName |
The process definition name of the historic task instance. |
processDefinitionNameLike |
The process definition name of the historic task instance, which matches the given value. |
processBusinessKey |
The process instance business key of the historic task instance. |
processBusinessKeyLike |
The process instance business key of the historic task instance that matches the given value. |
executionId |
The execution id of the historic task instance. |
taskDefinitionKey |
The task definition key for tasks part of a process |
taskName |
The task name of the historic task instance. |
taskNameLike |
The task name with like operator for the historic task instance. |
taskDescription |
The task description of the historic task instance |
taskDescriptionLike |
The task description with like operator for the historic task instance. |
taskDefinitionKey |
The task identifier from the process definition for the historic task instance. |
taskDeleteReason |
The task delete reason of the historic task instance. |
taskDeleteReasonLike |
The task delete reason with like operator for the historic task instance. |
taskAssignee |
The assignee of the historic task instance. |
taskAssigneeLike |
The assignee with like operator for the historic task instance. |
taskOwner |
The owner of the historic task instance. |
taskOwnerLike |
The owner with like operator for the historic task instance. |
taskInvolvedUser |
An involved user of the historic task instance. Where, InvolvedUser is the User ID. |
taskPriority |
The priority of the historic task instance. |
finished |
Indicates if the historic task instance is complete. |
processFinished |
Indicates if the process instance of the historic task instance is finished. |
parentTaskId |
An optional parent task ID of the historic task instance. |
dueDate |
Returns only historic task instances that have a due date equal to this date. |
dueDateAfter |
Returns only historic task instances that have a due date after this date. |
dueDateBefore |
Returns only historic task instances that have a due date before this date. |
withoutDueDate |
Returns only historic task instances that have no due-date. When false value is provided, this parameter is ignored. |
taskCompletedOn |
Returns only historic task instances that have been completed on this date. |
taskCompletedAfter |
Returns only historic task instances that have been completed after this date. |
taskCompletedBefore |
Return only historic task instances that have been completed before this date. |
taskCreatedOn |
Returns only historic task instances that were created on this date. |
taskCreatedBefore |
Returns only historic task instances that were created before this date. |
taskCreatedAfter |
Returns only historic task instances that were created after this date. |
includeTaskLocalVariables |
Indicates if the historic task instance local variables should be returned. |
includeProcessVariables |
Indicates if the historic task instance global variables should be returned. |
tenantId |
Returns historic task instances with the given tenantId. |
tenantIdLike |
Returns historic task instances with a tenantId like the given value. |
withoutTenantId |
If true, only returns historic task instances without a tenantId set. If false, withoutTenantId is ignored. |
A common use case is when a user wants to select another user or group, for example, when assigning a task.
To retrieve users:
GET api/enterprise/users
Use the following parameters:
filter: Filters by the user’s first and last name.
email: Retrieves users by email
externalId: Retrieves users by their external ID.
externalIdCaseInsensitive: Retrieves users by external ID, ignoring case.
externalId: Retrieves users by their external ID (set by the LDAP sync, if used)
excludeTaskId: Excludes users that are already part of this task.
excludeProcessId: Excludes users that are already part of this process instance.
Example response:
{ "size": 2, "total": 2, "start": 0, "data": [ { "id": 1, "firstName": null, "lastName": "Administrator", "email": "admin@app.activiti.com" }, { "id": 1000, "firstName": "John", "lastName": "Doe", "email": "johndoe@alfresco.com" } ] }
To retrieve a picture of a user:
GET api/enterprise/users/{userId}/picture
To retrieve groups:
GET api/enterprise/groups
with optional parameter filter that filters by group name.
Additional options:
externalId: Retrieves a group by their external ID.
externalIdCaseInsensitive: Retrieves a group by their external ID, ignoring case.
Example response:
{ "size": 2, "total": 2, "data": [ { "externalId": null, "name": "Engineering", "id": 2000 }, { "externalId": null, "name": "Marketing", "id": 2001 } ], "start": 0 }
Get the users for a given group:
GET api/enterprise/groups/{groupId}/users
Example response:
{ "size": 3, "total": 3, "data": [ { "email": "john@alfresco.com", "lastName": "Test", "firstName": "John", "id": 10 }, { "email": "mary@alfresco.com", "lastName": "Test", "firstName": "Mary", "id": 8 }, { "email": "patrick@alfresco.com", "lastName": "Test", "firstName": "Patrick", "id": 9 } ], "start": 0 }
With a json body that contains:
order : An array of user task filter IDs
Content such as documents and other files can be attached to process instances and tasks.
To retrieve the content attached to a process instance:
GET api/enterprise/process-instances/{processInstanceId}/content
By default, this will return all content: The related content (for example content uploaded via the UI in the "related content" section of the task detail page) and the field content (content uploaded as part of a form).
To only return the related content, add ?isRelatedContent=true to the url. Similarly, add ?isRelatedContent=false when the return response should include only field content.
Similarly, for a task:
GET api/enterprise/tasks/{taskId}/content
By default, this will return all content: The related content (for example content uploaded via the UI in the "related content" section of the task detail page) and the field content (content uploaded as part of a form).
To only return the related content, add ?isRelatedContent=true' to the url. Similarly, add ?isRelatedContent=false when the return response should include only field content.
Example response:
{ "size": 5, "total": 5, "start": 0, "data": [ { "id": 4000, "name": "tasks.PNG", "created": "2015-01-01T01:01:01.000+0000", "createdBy": { "id": 1, "firstName": "null", "lastName": "Admin", "email": "admin@app.activiti.com", "pictureId": 5 }, "relatedContent": true, "contentAvailable": true, "link": false, "mimeType": "image/png", "simpleType": "image", "previewStatus": "queued", "thumbnailStatus": "queued" } ] }
To get content metadata:
GET api/enterprise/content/{contentId}
To delete content:
DELETE api/enterprise/content/{contentId}
To get the actual bytes for content:
GET api/enterprise/content/{contentId}/raw
To upload content to a process instance:
POST api/enterprise/process-instances/{processInstanceId}/raw-content
where the body contains a multipart file. Add the isRelatedContent parameter to the url to set whether the content is related or not. For a process instance, this currently won’t have any influence on what is visible in the UI. Note that the default value for this parameter is false.
To upload content to a task:
POST api/enterprise/tasks/{taskId}/raw-content
where the body contains a multipart file. Add the isRelatedContent parameter to the url to set whether the content is related or not. If true, the content will show up in the "related content" section of the task details. Note that the default value for this parameter is false.
To relate content (eg from Alfresco) to a process instance:
POST api/enterprise/process-instances/{processInstanceId}/content
where the json body contains following properties:
name
link (boolean)
source
sourceId
mimeType
linkUrl
Add the isRelatedContent parameter to the url to set whether the content is related or not. If true, the content will show up in the "related content" section of the task details. Note that the default value for this parameter is true (different from the call above with regular content!).
Example body (from Alfresco OnPremise):
{ "name":"Image.png", "link":true, "source":"alfresco-1", "sourceId":"30358280-88de-436e-9d4d-8baa9dc44f17@swsdp", "mimeType":"image/png" }
To upload content for a task:
POST api/enterprise/process-instances/{taskId}/content
Where the json body contains following properties:
name
link (boolean)
source
sourceId
mimeType
linkUrl
In case of a start form with content fields, there is no task or process instance to relate to.
Following REST endpoints can be used:
POST api/enterprise/content/raw
To retrieve the thumbnail of a certain piece of content:
GET api/enterprise/content/{contentId}/rendition/thumbnail
These are operations to manage tenants, groups and users. This is useful for example to bootstrap environments with the correct identity data.
Following REST endpoints are only available for users that are either a tenant admin or a tenant manager.
Get all tenants (tenant manager only):
GET api/enterprise/admin/tenants
Create a new tenant (tenant manager only):
POST api/enterprise/admin/tenants
the json body of this post contains two properties: name and active (boolean).
Update a tenant:
PUT api/enterprise/admin/tenants/{tenantId}
the json body of this post contains two properties: name and active (boolean).
Get tenant details:
GET api/enterprise/admin/tenants/{tenantId}
Delete a tenant:
DELETE api/enterprise/admin/tenants/{tenantId}
Get tenant events:
GET api/enterprise/admin/tenants/{tenantId}/events
Get tenant logo:
GET api/enterprise/admin/tenants/{tenantId}/logo
Change tenant logo:
POST api/enterprise/admin/tenants/{tenantId}/logo
Following REST endpoints are only available for users that are either a tenant admin or a tenant manager.
Get a list of users:
GET api/enterprise/admin/users
with parameters
filter : Filters by user name.
status : Possible values are pending, inactive, active, deleted.
sort : Possible values are createdAsc, createdDesc, emailAsc or emailDesc (default createdAsc).
start : Used for paging.
size : Use for paging.
To create a new user:
POST api/enterprise/admin/users
with a json body that must have following properties:
firstName
lastName
password
status (possible values are pending, inactive, active, deleted)
type (enterprise or trial. Best to set this to enterprise)
tenantId
Update user details:
PUT api/enterprise/admin/users/{userId}
with a json body containing email, firstName and lastName
Update user password:
PUT api/enterprise/admin/users
with a json body like
{ "users" : [1098, 2045, 3049] "password" : "123" }
Note that the users property is an array of user ids. This allows for bulk changes.
Update user status:
PUT api/enterprise/admin/users
with a json body like
{ "users" : [1098, 2045, 3049] "status" : "inactive" }
Note that the users property is an array of user ids. This allows for bulk changes.
Update user tenant id (only possible for _tenant manager):
PUT api/enterprise/admin/users
with a json body like
{ "users" : [1098, 2045, 3049] "tenantId" : 1073 }
Note that the users property is an array of user ids. This allows for bulk changes.
The following REST endpoints are only available for users that are either a tenant admin or a tenant manager.
Internally, there are two types of groups:
Functional groups: Map to organizational units.
System groups: Provide users capabilities. When you assign a capability to a group, every member of that group is assigned with the capability.
Get all groups:
GET api/enterprise/admin/groups
Optional parameters:
tenantId : Useful to a Tenant Manager user
functional (boolean): Only return functional groups if true
Get group details:
GET api/enterprise/admin/groups/{groupId}
Example response:
{ "capabilities": [{ "name": "access-reports", "id": 1 }], "name": "analytics-users", "tenantId": 1, "users": [ { "tenantId": 1, "firstName": null, "password": null, "type": "enterprise", "company": null, "externalId": null, "capabilities": null, "tenantPictureId": null, "created": "2015-01-08T08:30:25.164+0000", "pictureId": null, "latestSyncTimeStamp": null, "tenantName": null, "lastName": "Administrator", "id": 1, "lastUpdate": "2015-01-08T08:30:25.164+0000", "email": "admin@app.activiti.com", "fullname": " Administrator", "groups": null }, { "tenantId": 1, "firstName": "John", "password": null, "type": "enterprise", "company": null, "externalId": null, "capabilities": null, "tenantPictureId": null, "created": "2015-01-08T13:22:36.198+0000", "pictureId": null, "latestSyncTimeStamp": null, "tenantName": null, "lastName": "Doe", "id": 1000, "lastUpdate": "2015-01-08T13:34:22.273+0000", "email": "johndoe@alfresco.com", "fullname": "John Doe", "groups": null } ], "id": 1, "groups": [], "externalId": null, "status": "active", "lastSyncTimeStamp": null, "type": 0, "parentGroupId": null }
Use the optional request parameter includeAllUsers (boolean value, by default true) to avoid getting all the users at once (not ideal if there are many users).
Use the following call:
GET api/enterprise/admin/groups/{groupId}/users?page=2&pageSize=20
Create new group:
POST api/enterprise/admin/groups
Where the json body contains following properties:
name
tenantId
type (0 for system group, 1 for functional group)
parentGroupId (only possible for functional groups. System groups can’t be nested)
Update a group:
PUT api/enterprise/admin/groups/{groupId}
Only the name property can be in the json body.
Delete a group:
DELETE api/enterprise/admin/groups/{groupId}
Add a user to a group:
POST api/enterprise/admin/groups/{groupId}/members/{userId}
Delete a user from a group:
DELETE api/enterprise/admin/groups/{groupId}/members/{userId}
Get the list of possible capabilities for a system group:
GET api/enterprise/admin/groups/{groupId}/potential-capabilities
Add a capability from previous list to the group:
POST api/enterprise/admin/groups/{groupId}/capabilities
where the json body contains one property capabilities that is an array of strings.
Remove a capability from a group:
DELETE api/enterprise/admin/groups/{groupId}/capabilities/{groupCapabilityId}
A tenant administrator can configure one or more Alfresco Content Services repositories to use when working with content. To retrieve the repositories configured for the tenant of the user used to do the request:
GET api/enterprise/profile/accounts/alfresco
which returns something like:
{ "size": 2, "total": 2, "data": [ { "name": "TS", "tenantId": 1, "id": 1, "accountUsername": "jbarrez", "created": "2015-03-26T14:24:35.506+0000", "shareUrl": "http://ts.alfresco.com/share", "lastUpdated": "2015-03-26T15:37:21.174+0000", "repositoryUrl": "http://ts.alfresco.com/alfresco", "alfrescoTenantId": "" }, { "name": "TsTest", "tenantId": 1, "id": 1000, "accountUsername": "jbarrez", "created": "2015-03-26T15:37:36.448+0000", "shareUrl": "http://tstest.alfresco.com/share", "lastUpdated": "2015-03-26T15:37:36.448+0000", "repositoryUrl": "http://tstest.alfresco.com/alfresco", "alfrescoTenantId": "" } ], "start": 0 }
Links:
[1] https://docs.alfresco.com/adminGuide.html
[2] https://www.youtube.com/watch?v=gyz2By2g1p8
[3] https://docs.alfresco.com/../topics/high_level_architecture.html
[4] https://docs.alfresco.com/../topics/embed_process_services.html
[5] https://docs.alfresco.com/../topics/maven_modules.html
[6] https://docs.alfresco.com/../topics/start_and_task_form_customization.html
[7] https://docs.alfresco.com/../topics/custom_form_fields.html
[8] https://docs.alfresco.com/../topics/custom_web_resources.html
[9] https://docs.alfresco.com/../topics/document_templates.html
[10] https://docs.alfresco.com/../topics/custom_logic.html
[11] https://docs.alfresco.com/../topics/custom_data_models.html
[12] https://docs.alfresco.com/../topics/custom_reports.html
[13] https://docs.alfresco.com/../topics/cookie_configuration.html
[14] https://docs.alfresco.com/../topics/custom_identity_synchronization.html
[15] https://docs.alfresco.com/../topics/security_configuration_overrides.html
[16] https://docs.alfresco.com/../topics/rest_api.html
[17] https://docs.alfresco.com/../concepts/welcome.html
[18] https://www.activiti.org/5.x/userguide/index.html
[19] https://docs.alfresco.com/cluster_configuration_and_monitoring.html
[20] https://www.alfresco.com/services/subscription/supported-platforms
[21] https://docs.alfresco.com/../topics/developmentGuide.html
[22] https://docs.alfresco.com/custom_web_resources.html
[23] https://docs.alfresco.com/../topics/example_1_static_image.html
[24] https://docs.alfresco.com/../topics/example_2_dynamic_image.html
[25] https://docs.alfresco.com/../topics/example_3_dynamic_pie_chart.html
[26] https://docs.angularjs.org/api/ng/directive/ngSrc
[27] https://github.com/fastly/epoch
[28] https://raw.githubusercontent.com/mbostock/d3/v3.5.6/d3.min.js
[29] https://raw.githubusercontent.com/fastly/epoch/0.6.0/epoch.min.js
[30] https://docs.alfresco.com/processing_document_generation_variables.html
[31] https://docs.alfresco.com/../topics/java_delegates.html
[32] https://docs.alfresco.com/../topics/spring_beans.html
[33] https://docs.alfresco.com/../topics/default_spring_beans.html
[34] https://docs.alfresco.com/../topics/hook_points.html
[35] https://docs.alfresco.com/../topics/custom_rest_endpoints.html
[36] https://docs.alfresco.com/../topics/custom_rule_expression_functions.html
[37] https://docs.alfresco.com/../topics/audit_log_bean_auditlogbean.html
[38] https://docs.alfresco.com/../topics/document_merge_bean_documentmergebean.html
[39] https://docs.alfresco.com/../topics/email_bean_emailbean.html
[40] https://docs.alfresco.com/../topics/user_info_bean_userinfobean.html
[41] https://docs.alfresco.com/../topics/login_logoutlistener.html
[42] https://docs.alfresco.com/../topics/process_engine_configuration_configurer.html
[43] https://docs.alfresco.com/../topics/rule_engine_configuration_configurer.html
[44] https://docs.alfresco.com/../topics/process_engine_event_listeners.html
[45] https://docs.alfresco.com/../topics/processing_document_generation_variables.html
[46] https://docs.alfresco.com/../topics/business_calendar.html
[47] http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html
[48] https://docs.alfresco.com/rule_engine_configuration_configurer.html
[49] https://www.elastic.co/guide/en/elasticsearch/guide/1.x/index.html
[50] https://www.elastic.co/guide/en/elasticsearch/reference/1.7/index.html
[51] https://docs.alfresco.com/../topics/implementing_custom_reports.html
[52] https://github.com/Alfresco/activiti-custom-reports
[53] https://docs.alfresco.com/externalIdentityManagement.html%23externalIdentityManagement
[54] https://docs.alfresco.com/../topics/customIdmExample.html
[55] https://docs.alfresco.com/../topics/synchronization_on_boot.html
[56] https://docs.alfresco.com/../topics/synchronization_log_entries.html
[57] https://docs.alfresco.com/../topics/custom_authentication.html
[58] https://docs.alfresco.com/securityConfigurationGlobalOverride.html
[59] https://docs.alfresco.com/../topics/securityConfigurationGlobalOverride.html
[60] https://docs.alfresco.com/../topics/passwordencoder_override.html
[61] https://docs.alfresco.com/customIdmExample.html
[62] https://docs.alfresco.com/../topics/rest_endpoints_security_overrides.html
[63] https://docs.alfresco.com/../topics/userdetailsservice_override.html
[64] https://docs.alfresco.com/../topics/enable-cors.html
[65] https://docs.alfresco.com/../topics/rest_api_authorization.html
[66] https://docs.alfresco.com/../topics/rest_api_explorer.html
[67] https://docs.alfresco.com/../concepts/RAML-support.html
[68] https://docs.alfresco.com/../topics/process_services_api.html
[69] https://docs.alfresco.com/../topics/process_engine_rest_api.html
[70] https://docs.alfresco.com/../topics/history.html
[71] https://docs.alfresco.com/enabling-cors.html
[72] https://docs.alfresco.com/../concepts/OAuth-overview.html
[73] https://docs.alfresco.com/../concepts/OAuth-features.html
[74] https://docs.alfresco.com/../tasks/install-OAuth.html
[75] https://docs.alfresco.com/../concepts/config-OAuth.html
[76] https://docs.alfresco.com/../concepts/config-OAuth-server.html
[77] https://docs.alfresco.com/../concepts/running-OAuth-server.html
[78] https://docs.alfresco.com/../concepts/using-OAuth-server.html
[79] https://artifacts.alfresco.com/nexus/content/repositories/activiti-enterprise-releases/org/alfresco/alfresco-oauth2/1.0.0/alfresco-oauth2-1.0.0.war
[80] https://docs.alfresco.com/../concepts/register-OAuth.html
[81] https://docs.alfresco.com/../concepts/config-OAuth-client.html
[82] https://docs.alfresco.com/../concepts/using-OAuth-client.html
[83] https://docs.alfresco.com/config-OAuth-client.html
[84] https://docs.alfresco.com/config-OAuth-server.html
[85] https://tools.ietf.org/html/rfc7662#section-2.2
[86] https://tools.ietf.org/html/rfc7662
[87] http://stackoverflow.com/questions/12296017/how-to-validate-an-oauth-2-0-access-token-for-a-resource-server
[88] http://tools.ietf.org/html/rfc6749#section-4.2
[89] http://tools.ietf.org/html/rfc6749#section-4.3
[90] http://tools.ietf.org/html/rfc6749#section-4.4
[91] https://tools.ietf.org/html/rfc6749#section-4.5
[92] https://openapis.org/
[93] http://localhost:8080/activiti-app/api-explorer.html
[94] https://activiti.alfresco.com/activiti-app/api-explorer.html
[95] https://raml.org
[96] http://apiworkbench.com/docs
[97] https://www.mulesoft.com/platform/api/anypoint-designer
[98] http://raml.org/projects/projects
[99] https://docs.alfresco.com/../topics/server_information.html
[100] https://docs.alfresco.com/../topics/profile.html
[101] https://docs.alfresco.com/../topics/runtime_apps.html
[102] https://docs.alfresco.com/../topics/app_definitions_list.html
[103] https://docs.alfresco.com/../topics/app_import_and_export.html
[104] https://docs.alfresco.com/../topics/app_publish_and_deploy.html
[105] https://docs.alfresco.com/../topics/process_definition_models_list.html
[106] https://docs.alfresco.com/../topics/model_details_and_history.html
[107] https://docs.alfresco.com/../topics/bpmn_2_0_import_and_export.html
[108] https://docs.alfresco.com/../topics/process_definitions.html
[109] https://docs.alfresco.com/../topics/start_form.html
[110] https://docs.alfresco.com/../topics/start_process_instance.html
[111] https://docs.alfresco.com/../topics/process_instance_list.html
[112] https://docs.alfresco.com/../topics/get_process_instance_details.html
[113] https://docs.alfresco.com/../topics/delete_a_process_instance.html
[114] https://docs.alfresco.com/../topics/process_instance_audit_log_as_json.html
[115] https://docs.alfresco.com/../topics/process_instance_variables.html
[116] https://docs.alfresco.com/../topics/process_instance_identity_links.html
[117] https://docs.alfresco.com/../topics/task_list.html
[118] https://docs.alfresco.com/../topics/task_details.html
[119] https://docs.alfresco.com/../topics/task_form.html
[120] https://docs.alfresco.com/../topics/create_a_standalone_task.html
[121] https://docs.alfresco.com/../topics/task_actions.html
[122] https://docs.alfresco.com/../topics/task_variables.html
[123] https://docs.alfresco.com/../topics/task_identity_links.html
[124] https://docs.alfresco.com/../topics/user_task_filters.html
[125] https://docs.alfresco.com/../topics/comments.html
[126] https://docs.alfresco.com/../topics/checklists.html
[127] https://docs.alfresco.com/../topics/task_audit_info_as_json.html
[128] http://activiti.org/userguide/index.html#_rest_api
[129] https://docs.alfresco.com/../topics/historic_process_instance_queries.html
[130] https://docs.alfresco.com/../topics/get_historic_process_instances.html
[131] https://docs.alfresco.com/../topics/get_historic_task_instances.html
[132] https://docs.alfresco.com/../topics/user_and_group_lists.html
[133] https://docs.alfresco.com/../topics/content.html
[134] https://docs.alfresco.com/../topics/thumbnails.html
[135] https://docs.alfresco.com/../topics/identity_management.html
[136] https://docs.alfresco.com/../topics/tenants_tab.html
[137] https://docs.alfresco.com/../topics/users_tab.html
[138] https://docs.alfresco.com/../topics/capabilities_tab.html
[139] https://docs.alfresco.com/../topics/organization_tab.html
[140] https://docs.alfresco.com/../topics/tenants.html
[141] https://docs.alfresco.com/../topics/users.html
[142] https://docs.alfresco.com/../topics/groups.html
[143] https://docs.alfresco.com/../topics/content_services_repositories.html
[144] https://docs.alfresco.com/../concepts/Landing-page.html