Tuesday, April 7, 2009

Working with jBPM workflows in Alfresco - Part 2: Embedded Alf - SDK

This part focuses on extending the previous blog post: Working with Workflows in Alfresco - Part 1: jBPM to run as an Embedded Alf SDK project, and to configure and use Alfresco scripts as actions in the workflows.

Step 1 - Install SDK
Make sure you install the SDK according to the documentation on the Alfresco Wiki
Import the SDK projects into the same workspace you are using for this tutorial

Step 2 - Configure Project to Run with Embedded Alfresco
  1. Go to project properties > Java buid path > Projects tab and select 'add' to add SDK AlfrescoEmbedded
  2. From the SDK FirstFoundationClient, copy alfresco.extensions package from the 'source' source folder and paste into the 'src/main/config' source directory of your project (i.e. 'orchestration-example'). This will include the files 'custom-alfresco-shared.xml', 'custom-repository-context.xml' and 'custom-repository.properties'
  3. From the SDK FirstFoundationClient, copy the org.alfresco.sample package and paste into the src/main/java source folder of your project. This will give us something to test running as Alfresco Embedded
  4. Update the 'custom-repository.properties' to point dir.root to your Alfresco installation
  5. Remove the JBPM library from project properties, as it will conflict with the Alfresco version of these dependencies
  6. I recommend that you modify the log4j.properties to set the root logger to INFO, CONSOLE not DEBUG, CONSOLE or you will be watching the log messages for half the day
Step 3 - Test it out
Run the FirstFoundationClient as Java Application. There should be no 'red ink' and the line near the end of the log messages should read something like 'Alfresco started (Labs): Current version 3.0.0 (Stable 1526) schema 1002 - Installed version 3.1.0 (142) schema 1008'
If you see this line, likely you were able to run using Alfresco Embedded SDK

Step 4 - Create an Embedded Alf test

To test within the Alfresco context using Embedded approach, your workflow must be deployed, and you must run your test code, like the FirstFoundationClient example within a transaction service callback.
We will create a base test class, extending TestCase of JUnit 3 to simplify writing tests like this.
package com.sample;

import junit.framework.TestCase;

import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;

public abstract class EmbeddedAlfTestBase extends TestCase {

Logger logger = Logger.getLogger(EmbeddedAlfTestBase.class);
protected ServiceRegistry serviceRegistry;
protected TransactionService transactionService;
protected ApplicationContext ctx;

public void setUp() throws Exception {
ctx = ApplicationContextHelper.getApplicationContext();
serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
transactionService = serviceRegistry.getTransactionService();
}

public void tearDown() throws Exception {
}

public void runTestInEmbeddedAlf(RetryingTransactionCallback<Object> testWork) throws Exception {
transactionService.getRetryingTransactionHelper().doInTransaction(testWork);
}
}
This code gets the application context and gets some services from Spring, including the serviceRegistry and the transactionService. The helper method, runtestInEmbeddedAlf takes a class of type RetryingtransactionCallback and runs it it using the transactionService's RetryingTransactionHelper doInTransaction(callback) method.

We will subclass this base test class and write our process test.
  1. Create a class PublishContentBasicProcessAlfTest, copying from PublishContentBasicProcessTest created in the previous blog entry.
  2. Change it to extend from EmbeddedAlfTestBase class
  3. Rename testSimpleProcess to a signature 'public void doTestSimpleProcess(ServiceRegistry serviceRegistry) throws Exception'
  4. Create a new testSimpleProcess like the following:
    public void testSimpleProcess() throws Exception {
RetryingTransactionCallback<Object> publishContentWFExistsCB = new RetryingTransactionCallback<Object>() {
public Object execute() throws Exception {
doTestSimpleProcess(serviceRegistry);
return null;
}
};
this.runTestInEmbeddedAlf(publishContentWFExistsCB);
}
Now run 'PublishContentBasicProcessAlfTest' as a JUnit test.
This should run as an alfresco project properly. But we are not leveraging Alfresco's services yet.

Step 5 - Test in Alf Using Workflow Service (foundation services)

What we are going to do:
  1. Authenticate and deploy in setUp for test
  2. Start workflow using workflow service
  3. Signal workflow using workflow service
Create a class PublishContentBasicProcessAlfTest extends EmbeddedAlfTestBase
To start, we will create a setUp() method to set us up for testing, like authenticating and deploying the service
    private WorkflowService workflowService;

public void setUp() throws Exception {
super.setUp();
workflowService = serviceRegistry.getWorkflowService();

authenticate("admin","admin");

deployDefinition(PROCESS_DEF_FILE);
}

We get the workflow service from the service registry (obtained in the setUp of the EmbeddedAlfTestBase class). Then we authenticate, and then deploy our process definition 'publichContentBasic/processdefinition.xml'.
Lets look at the methods we need to create:

authenticate method
    private void authenticate(String user, String password) {
AuthenticationService authenticationService = serviceRegistry.getAuthenticationService();
authenticationService.authenticate(user, password.toCharArray());

}

We get the AuthenticationService, and call authenticate. We need to do this first or else we wont be able to deploy our workflow.

deployDefinition method
    private WorkflowDeployment deployDefinition(String processDefName) {
//Deploy definition
String engineId = ENGINE_ID;
InputStream workflowDefinition = getClass().getResourceAsStream("/"+processDefName);
String mimeType = XML_MIMETYPE;
return workflowService.deployDefinition(engineId, workflowDefinition, mimeType);
}

We use the deployDefinition method of the workflow service. THis requires us to pass an engineId. this Id for jbpm is 'jbpm'. The mime type is 'text/xml'. The stream is obtained as a resource from this class loader by reading the 'publishContentBasic/processdefinition.xml' file.

Now we start the test like we did before, using the transactionservice and its callback:
    public void testSimpleProcess() throws Exception {
RetryingTransactionCallback<Object> publishContentWFExistsCB = new RetryingTransactionCallback<Object>() {
public Object execute() throws Exception {
doTestSimpleProcessInAlf();
return null;
}
};
this.runTestInEmbeddedAlf(publishContentWFExistsCB);

}

public void doTestSimpleProcessInAlf() throws Exception {

}


Ok. Now that we've got the test method ready. Lets put something in it. The first thing we have to do is start our workflow:
    public void doTestSimpleProcessInAlf() throws Exception {


NodeRef content = null;
String wfAssigneeName = "admin";
String workflowName = ENGINE_ID+"$"+PROCESS_DEF_NAME;

WorkflowPath wfPath = startWorkflow(content, wfAssigneeName, workflowName);
assertNotNull(wfPath);

assertEquals(
"Instance is in start state",
wfPath.node.name,
"start");

This calls the startWorkflow helper method with the content to go in the workflow package (currently null for testing purposes). We also need a name to assign the workflow to. We use 'admin'. And, finally we need the workflow name to start. Alfresco uses a naming convention prepending the engine id and a '$' to the beginning of BPM engine objects. The value for jbpm is 'jbpm'.

startWorkflow method
    private WorkflowPath startWorkflow(NodeRef content, String wfAssigneeName, String workflowName) throws Exception {
//Start workflow
NodeRef wfPackage = workflowService.createPackage(content );

PersonService personService = serviceRegistry.getPersonService();
NodeRef assigneeNodeRef = personService.getPerson(wfAssigneeName );

Map<QName, Serializable> workflowProps = new HashMap<QName, Serializable>(16);
workflowProps.put(WorkflowModel.ASSOC_PACKAGE, wfPackage);
workflowProps.put(WorkflowModel.ASSOC_ASSIGNEE, assigneeNodeRef);

// get the moderated workflow

WorkflowDefinition wfDefinition = workflowService.getDefinitionByName(workflowName );
if (wfDefinition == null) {
// handle workflow definition does not exist
throw new Exception("noworkflow: " + workflowName);
}

// start the workflow
WorkflowPath wfPath = workflowService.startWorkflow(wfDefinition.getId(), workflowProps);
return wfPath;
}
In this code, we create the package for the workflow that should contain the content we are workflowing. This is a content manager after all :). Next we get the user we will assign this workflow too. Since this process has no 'tasks', nothing will be noticable to the user. For this kind of process orchestration, the 'admin' user will do nicely. Perhaps in a real system, we want to create a special user just for back office processing like this?

Next we get the definition from the service using the given name 'jbpm$publishContentBasic'. Finally, we use the service to start the workflow, passing the definition and the workflow package. The startWorkflow service returns a WorkflowPath object. This corresponds to the workflow token of the execution of the workflow.

We can test this now to make sure we can deploy and start our workflow.

Next, we will signal the workflow to transition to the next state. This code snippet should be added to the end of the doTestSimpleProcessInAlf method
        // Move the process instance from its start state to the first state.
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in reqested state",
wfPath.node.name,
"requested");

Here we call the 'signalTransition' helper method passing the current path (token) and a transition name to take. Our helper method takes the first transition from the current state if no transition name is supplied.
signalTransition method
    private WorkflowPath signalTransition(WorkflowPath wfPath, String transitionName) throws Exception {
String wfPathId = wfPath.id;
WorkflowTransition[] wfTransitions = wfPath.node.transitions;
String wfTransitionId = null;
if (transitionName == null || transitionName.trim().length()==0) {
WorkflowTransition wfTransition = wfTransitions[0];
wfTransitionId = wfTransition.id;
} else {
int i = 0;
for (i = 0; i<wfTransitions.length; i++) {
if (wfTransitions[i].title.equals(transitionName)) break;
}
if (i > wfTransitions.length) throw new Exception("Failed to find transition with nanme '"+transitionName+"'");
WorkflowTransition wfTransition = wfTransitions[i];
wfTransitionId = wfTransition.id;
}

wfPath = workflowService.signal(wfPathId, wfTransitionId);
return wfPath;
}

Ok. This pattern can be repeated to transition to the next states to complete our test
        //move to processing state
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in processing state",
"processing",
wfPath.node.name);

//move to succeeded state
wfPath = signalTransition(wfPath, "to_succeeded");
assertEquals(
"Instance is in processing state",
"succeeded",
wfPath.node.name);

// Move the process instance to the end state. The configured action
// should execute again. The message variable contains a new value.
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in end state",
"end",
wfPath.node.name);
}



This completes the code to use the workflow service foundation client approach to deploy, start and signal our workflow.

Here is the complete code of the PublishContentBasicAlfTest
package com.sample;

import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.workflow.WorkflowDefinition;
import org.alfresco.service.cmr.workflow.WorkflowDeployment;
import org.alfresco.service.cmr.workflow.WorkflowPath;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.cmr.workflow.WorkflowTransition;
import org.alfresco.service.namespace.QName;

public class PublishContentBasicProcessAlfTest extends EmbeddedAlfTestBase {

private static final String XML_MIMETYPE = "text/xml";
private static final String PROCESS_DEF_FILE = "publishContentBasic/processdefinition.xml";
private static final String PROCESS_DEF_NAME = "publishContentBasic";
private static final String ENGINE_ID = "jbpm";

private WorkflowService workflowService;

public void setUp() throws Exception {
super.setUp();
workflowService = serviceRegistry.getWorkflowService();

authenticate("admin","admin");

deployDefinition(PROCESS_DEF_FILE);
}

public void tearDown() throws Exception {
super.tearDown();
}

public void testSimpleProcess() throws Exception {
RetryingTransactionCallback<Object> publishContentWFExistsCB = new RetryingTransactionCallback<Object>() {
public Object execute() throws Exception {
doTestSimpleProcessInAlf();
return null;
}
};
this.runTestInEmbeddedAlf(publishContentWFExistsCB);

}

public void doTestSimpleProcessInAlf() throws Exception {


NodeRef content = null;
String wfAssigneeName = "admin";
String workflowName = ENGINE_ID+"$"+PROCESS_DEF_NAME;

WorkflowPath wfPath = startWorkflow(content, wfAssigneeName, workflowName);
assertNotNull(wfPath);

assertEquals(
"Instance is in start state",
wfPath.node.name,
"start");

// Move the process instance from its start state to the first state.
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in reqested state",
wfPath.node.name,
"requested");

//move to processing state
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in processing state",
"processing",
wfPath.node.name);

//move to succeeded state
wfPath = signalTransition(wfPath, "to_succeeded");
assertEquals(
"Instance is in processing state",
"succeeded",
wfPath.node.name);

// Move the process instance to the end state. The configured action
// should execute again. The message variable contains a new value.
wfPath = signalTransition(wfPath, null);
assertEquals(
"Instance is in end state",
"end",
wfPath.node.name);
}

private WorkflowPath startWorkflow(NodeRef content, String wfAssigneeName, String workflowName) throws Exception {
//Start workflow
NodeRef wfPackage = workflowService.createPackage(content );

PersonService personService = serviceRegistry.getPersonService();
NodeRef assigneeNodeRef = personService.getPerson(wfAssigneeName );

Map<QName, Serializable> workflowProps = new HashMap<QName, Serializable>(16);
workflowProps.put(WorkflowModel.ASSOC_PACKAGE, wfPackage);
workflowProps.put(WorkflowModel.ASSOC_ASSIGNEE, assigneeNodeRef);

// get the moderated workflow

WorkflowDefinition wfDefinition = workflowService.getDefinitionByName(workflowName );
if (wfDefinition == null) {
// handle workflow definition does not exist
throw new Exception("noworkflow: " + workflowName);
}

// start the workflow
WorkflowPath wfPath = workflowService.startWorkflow(wfDefinition.getId(), workflowProps);
return wfPath;
}

private WorkflowDeployment deployDefinition(String processDefName) {
//Deploy definition
String engineId = ENGINE_ID;
InputStream workflowDefinition = getClass().getResourceAsStream("/"+processDefName);
String mimeType = XML_MIMETYPE;
return workflowService.deployDefinition(engineId, workflowDefinition, mimeType);
}

private WorkflowPath signalTransition(WorkflowPath wfPath, String transitionName) throws Exception {
String wfPathId = wfPath.id;
WorkflowTransition[] wfTransitions = wfPath.node.transitions;
String wfTransitionId = null;
if (transitionName == null || transitionName.trim().length()==0) {
WorkflowTransition wfTransition = wfTransitions[0];
wfTransitionId = wfTransition.id;
} else {
int i = 0;
for (i = 0; i<wfTransitions.length; i++) {
if (wfTransitions[i].title.equals(transitionName)) break;
}
if (i > wfTransitions.length) throw new Exception("Failed to find transition with nanme '"+transitionName+"'");
WorkflowTransition wfTransition = wfTransitions[i];
wfTransitionId = wfTransition.id;
}

wfPath = workflowService.signal(wfPathId, wfTransitionId);
return wfPath;
}

private boolean authenticate(String user, String password) {
AuthenticationService authenticationService = serviceRegistry.getAuthenticationService();
authenticationService.authenticate(user, password.toCharArray());
return authenticationService.authenticationExists(user);
}
}



In the next blog post, we will use alfresco scripts in the workflow definition we have just created. And finally, for orchestration, we will interact with JMS.

No comments: