diff --git a/config/test/alfresco/parallel_loop_review_processdefinition.xml b/config/test/alfresco/parallel_loop_review_processdefinition.xml new file mode 100644 index 0000000000..47ce726990 --- /dev/null +++ b/config/test/alfresco/parallel_loop_review_processdefinition.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + #{bpm_assignees} + reviewer + + + + + + + + + + + #{reviewer} + + + + + + + + + + + + + + + + + + + + + + #{wf_actualPercent >= wf_requiredApprovePercent} + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/workflow/WorkflowModel.java b/source/java/org/alfresco/repo/workflow/WorkflowModel.java index 5f2a7e2a2d..e7895ccea2 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowModel.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowModel.java @@ -66,6 +66,7 @@ public interface WorkflowModel static final QName PROP_WORKFLOW_PRIORITY = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "workflowPriority"); static final QName PROP_WORKFLOW_DUE_DATE = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "workflowDueDate"); static final QName ASSOC_ASSIGNEE = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "assignee"); + static final QName ASSOC_ASSIGNEES = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "assignees"); // workflow package static final QName ASPECT_WORKFLOW_PACKAGE = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "workflowPackage"); diff --git a/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java index aa1d2eff1c..b3811c6a6b 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java @@ -20,6 +20,7 @@ package org.alfresco.repo.workflow.jbpm; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -47,7 +48,6 @@ public class ForEachFork extends JBPMSpringActionHandler private Element foreach; private String var; - /* (non-Javadoc) * @see org.alfresco.repo.workflow.jbpm.JBPMSpringActionHandler#initialiseHandler(org.springframework.beans.factory.BeanFactory) */ @@ -63,7 +63,6 @@ public class ForEachFork extends JBPMSpringActionHandler * @param executionContext * @throws Exception */ - @SuppressWarnings("unchecked") public void execute(final ExecutionContext executionContext) throws Exception { @@ -76,57 +75,7 @@ public class ForEachFork extends JBPMSpringActionHandler throw new WorkflowException("forEach has not been provided"); } - // build "for each" collection - List forEachColl = null; - String forEachCollStr = foreach.getTextTrim(); - if (forEachCollStr != null) - { - if (forEachCollStr.startsWith("#{")) - { - String expression = forEachCollStr.substring(2, forEachCollStr.length() -1); - Object eval = AlfrescoJavaScript.executeScript(executionContext, services, expression, null); - if (eval == null) - { - throw new WorkflowException("forEach expression '" + forEachCollStr + "' evaluates to null"); - } - - // expression evaluates to string - if (eval instanceof String) - { - String[] forEachStrs = ((String)eval).trim().split(","); - forEachColl = new ArrayList(forEachStrs.length); - for (String forEachStr : forEachStrs) - { - forEachColl.add(forEachStr); - } - } - - // expression evaluates to Node array - else if (eval instanceof Serializable[]) - { - Serializable[] nodes = (Serializable[])eval; - forEachColl = new ArrayList(nodes.length); - for (Serializable node : nodes) - { - if (node instanceof NodeRef) - { - forEachColl.add(new JBPMNode((NodeRef)node, services)); - } - } - } - - // expression evaluates to collection - else if (eval instanceof Collection) - { - forEachColl = (List)eval; - } - - } - } - else - { - forEachColl = (List)FieldInstantiator.getValue(List.class, foreach); - } +// Collection forEachColl = buildForEachCollection(executionContext); if (var == null || var.length() == 0) { @@ -137,31 +86,30 @@ public class ForEachFork extends JBPMSpringActionHandler // create forked paths // - Token rootToken = executionContext.getToken(); Node node = executionContext.getNode(); List forkTransitions = new ArrayList(); - // first, create a new token and execution context for each item in list - for (int i = 0; i < node.getLeavingTransitions().size(); i++) + Collection forEachColl = buildForEachCollection(executionContext); + + // Create a new token and execution context for each node transition and item in list + List nodeTransitions = node.getLeavingTransitions(); + for (Transition noderansition : nodeTransitions) { - Transition transition = (Transition) node.getLeavingTransitions().get(i); - - for (int iVar = 0; iVar < forEachColl.size(); iVar++) + int iVar = 0; + for (Object item: forEachColl) { // create child token to represent new path - String tokenName = getTokenName(rootToken, transition.getName(), iVar); - Token loopToken = new Token(rootToken, tokenName); - loopToken.setTerminationImplicit(true); - executionContext.getJbpmContext().getSession().save(loopToken); - + Token loopToken = buildChildToken(executionContext, noderansition, iVar); + iVar++; + // assign variable within path final ExecutionContext newExecutionContext = new ExecutionContext(loopToken); - newExecutionContext.getContextInstance().createVariable(var, forEachColl.get(iVar), loopToken); + newExecutionContext.getContextInstance().createVariable(var, item, loopToken); // record path & transition ForkedTransition forkTransition = new ForkedTransition(); forkTransition.executionContext = newExecutionContext; - forkTransition.transition = transition; + forkTransition.transition = noderansition; forkTransitions.add(forkTransition); } } @@ -175,6 +123,74 @@ public class ForEachFork extends JBPMSpringActionHandler } } + private Token buildChildToken(final ExecutionContext executionContext, Transition noderansition, + int iVar) + { + Token rootToken = executionContext.getToken(); + String tokenName = getTokenName(rootToken, noderansition.getName(), iVar); + Token loopToken = new Token(rootToken, tokenName); + loopToken.setTerminationImplicit(true); + executionContext.getJbpmContext().getSession().save(loopToken); + return loopToken; + } + + private Collection buildForEachCollection(final ExecutionContext executionContext) + { + // build "for each" collection + String text = foreach.getTextTrim(); + if (text != null && text.startsWith("#{")) + { + return evaluateForEachExpression(executionContext, text); + } + return (Collection) FieldInstantiator.getValue(List.class, foreach); + } + + private Collection evaluateForEachExpression(final ExecutionContext executionContext, String forEachText) + { + String expression = forEachText.substring(2, forEachText.length() -1); + Object result = AlfrescoJavaScript.executeScript(executionContext, services, expression, null); + if (result == null) + { + throw new WorkflowException("forEach expression '" + forEachText + "' evaluates to null"); + } + // expression evaluates to string + if (result instanceof String) + { + return buildStrings((String)result); + } + // expression evaluates to Node array + else if (result instanceof Serializable[]) + { + return buildJbpmNodes((Serializable[]) result); + } + // expression evaluates to collection + else if (result instanceof Collection) + { + return (Collection)result; + } + else return null; + } + + private List buildStrings(String result) + { + String[] results = result.trim().split(","); + return Arrays.asList(results); + } + + private List buildJbpmNodes(Serializable[] nodes) + { + List jbpmNodes = new ArrayList(nodes.length); + for (Serializable node : nodes) + { + if (node instanceof NodeRef) + { + JBPMNode jbpmNode = new JBPMNode((NodeRef)node, services); + jbpmNodes.add(jbpmNode); + } + } + return jbpmNodes; + } + /** * Create a token name * @@ -184,33 +200,53 @@ public class ForEachFork extends JBPMSpringActionHandler */ protected String getTokenName(Token parent, String transitionName, int loopIndex) { - String tokenName = null; - if (transitionName != null && transitionName.length() > 0) + String suffix = "." + loopIndex; + if (transitionName == null || transitionName.isEmpty()) { - if (!parent.hasChild(transitionName)) - { - tokenName = transitionName; - } - else - { - int i = 2; - tokenName = transitionName + Integer.toString(i); - while (parent.hasChild(tokenName)) - { - i++; - tokenName = transitionName + Integer.toString(i); - } - } - } - else - { - // no transition name + // No transition name int size = (parent.getChildren() != null) ? parent.getChildren().size() + 1 : 1; - tokenName = "FOREACHFORK" + Integer.toString(size); + return buildTokenName("FOREACHFORK", suffix, size); + } + return findFirstAvailableTokenName(parent, transitionName, suffix); + } + + private String findFirstAvailableTokenName(Token parent, String transitionName, String suffix) + { + int i = 1; + while (true) + { + String tokenName = buildTokenName(transitionName, suffix, i); + if(!parent.hasChild(tokenName)) + { + return tokenName; + } + i++; } - return tokenName + "." + loopIndex; } + private String buildTokenName(String prefix, String suffix, int count) + { + String countStr = count<2 ? "": Integer.toString(count); + return prefix + countStr + suffix; + } + + /** + * Sets the list of objects to be iterated over. + * @param foreach the list of objects to set + */ + public void setForeach(Element foreach) + { + this.foreach = foreach; + } + + /** + * Set the name of the variable to which the eleements of foreach are assigned. + * @param var the variable name to set + */ + public void setVar(String var) + { + this.var = var; + } /** * Fork Transition diff --git a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java index fd578fb66d..e4c9fc9c4a 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java @@ -21,25 +21,22 @@ package org.alfresco.repo.workflow.jbpm; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.springframework.extensions.surf.util.I18NUtil; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; -import org.alfresco.repo.security.authentication.AuthenticationComponent; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.workflow.BPMEngineRegistry; import org.alfresco.repo.workflow.TaskComponent; import org.alfresco.repo.workflow.WorkflowComponent; import org.alfresco.repo.workflow.WorkflowModel; import org.alfresco.repo.workflow.WorkflowPackageComponent; -import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; +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.WorkflowException; @@ -49,8 +46,10 @@ import org.alfresco.service.cmr.workflow.WorkflowTask; import org.alfresco.service.cmr.workflow.WorkflowTaskState; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; -import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.BaseAlfrescoSpringTest; +import org.alfresco.util.PropertyMap; import org.springframework.core.io.ClassPathResource; +import org.springframework.extensions.surf.util.I18NUtil; /** @@ -58,22 +57,39 @@ import org.springframework.core.io.ClassPathResource; * * @author davidc */ -public class JBPMEngineTest extends BaseSpringTest +public class JBPMEngineTest extends BaseAlfrescoSpringTest { - AuthenticationComponent authenticationComponent; - NodeService nodeService; - WorkflowComponent workflowComponent; - TaskComponent taskComponent; - WorkflowPackageComponent packageComponent; - WorkflowDefinition testWorkflowDef; - NodeRef testNodeRef; - + /** + * + */ + private static final String USER3 = "JbpmEngineTestJoe"; + /** + * + */ + private static final String USER2 = "JbpmEngineTestJane"; + /** + * + */ + private static final String USER1 = "JbpmEngineTestJohn"; + private WorkflowComponent workflowComponent; + private TaskComponent taskComponent; + private WorkflowPackageComponent packageComponent; + private PersonService personService; + private WorkflowDefinition testWorkflowDef; + private NodeRef person1; + private NodeRef person2; + private NodeRef person3; + + @SuppressWarnings("deprecation") @Override protected void onSetUpInTransaction() throws Exception { - // run as system - authenticationComponent = (AuthenticationComponent)applicationContext.getBean("authenticationComponent"); - authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName()); + super.onSetUpInTransaction(); + + personService = (PersonService) applicationContext.getBean("PersonService"); + person1 = createPerson(USER1); + person2 = createPerson(USER2); + person3 = createPerson(USER3); BPMEngineRegistry registry = (BPMEngineRegistry)applicationContext.getBean("bpm_engineRegistry"); workflowComponent = registry.getWorkflowComponent("jbpm"); @@ -92,21 +108,10 @@ public class JBPMEngineTest extends BaseSpringTest assertEquals("jbpm$test", testWorkflowDef.name); assertEquals("1", testWorkflowDef.version); assertTrue(workflowComponent.isDefinitionDeployed(processDef.getInputStream(), MimetypeMap.MIMETYPE_XML)); - - // get valid node ref - nodeService = (NodeService)applicationContext.getBean(ServiceRegistry.NODE_SERVICE.getLocalName()); - testNodeRef = nodeService.getRootNode(new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore")); - nodeService.setProperty(testNodeRef, ContentModel.PROP_CREATED, new Date()); + + authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName()); } - - @Override - protected void onTearDownInTransaction() - { - authenticationComponent.clearCurrentSecurityContext(); - } - - public void testGetWorkflowDefinitions() { List workflowDefs = workflowComponent.getDefinitions(); @@ -135,6 +140,7 @@ public class JBPMEngineTest extends BaseSpringTest } catch(WorkflowException e) { + // Do nothing. } // TODO: Determine why process definition is loaded, even though it doesn't exist @@ -325,7 +331,7 @@ public class JBPMEngineTest extends BaseSpringTest } - public void testCancelWorkflowInstance() + public void testCancelWorkflowInstance() throws Exception { WorkflowDefinition workflowDef = getTestDefinition(); workflowComponent.startWorkflow(workflowDef.id, null); @@ -347,11 +353,80 @@ public class JBPMEngineTest extends BaseSpringTest assertEquals(0, tasks1.size()); } - + + /** + * See Alf-2764 in Jira. + * @throws Exception + */ + public void testCancelForEachFork() throws Exception + { + // Deploy Parallel Loop Review process definition. + ClassPathResource processDef = new ClassPathResource("test/alfresco/parallel_loop_review_processdefinition.xml"); + WorkflowDeployment deployment = workflowComponent.deployDefinition(processDef.getInputStream(), + MimetypeMap.MIMETYPE_XML); + WorkflowDefinition parallelDef = deployment.getDefinition(); + assertNotNull(parallelDef); + + // Set Current User to USER1. + AuthenticationUtil.setFullyAuthenticatedUser(USER1); + + // Set up parameters + QName approvePercentName = QName.createQName(NamespaceService.WORKFLOW_MODEL_1_0_URI, "requiredApprovePercent"); + NodeRef pckgNode = packageComponent.createPackage(null); + List assignees = Arrays.asList(person1, person2, person3); + Map parameters = new HashMap(); + parameters.put(WorkflowModel.ASSOC_ASSIGNEES, (Serializable) assignees); + parameters.put(WorkflowModel.ASSOC_PACKAGE, pckgNode); + parameters.put(approvePercentName, 60f ); + + // Start workflow + WorkflowPath path = workflowComponent.startWorkflow(parallelDef.getId(), parameters); + WorkflowTask startTask = workflowComponent.getTasksForWorkflowPath(path.getId()).get(0); + taskComponent.endTask(startTask.getId(), null); + checkInstanceExists(path.instance.getId(), parallelDef.getId(), true); + + // Set all users to reject document. + ParallelReject(USER1); + ParallelReject(USER2); + ParallelReject(USER3); + + // Send review back round the loop. + List tasks = workflowComponent.getTasksForWorkflowPath(path.getId()); + assertEquals(1, tasks.size()); + taskComponent.endTask(tasks.get(0).getId(), "again"); + + // Try to cancel workflow + WorkflowInstance cancelledWf = workflowComponent.cancelWorkflow(path.getInstance().getId()); + checkInstanceExists(cancelledWf.getId(), parallelDef.getId(), false); + } + + private void checkInstanceExists(String instanceId, String defId, boolean expected) + { + boolean match=false; + List activeWfs = workflowComponent.getActiveWorkflows(defId); + for (WorkflowInstance instance : activeWfs) + { + if(instance.getId().equals(instanceId)) + { + match = true; + break; + } + } + assertEquals( expected, match); + } + + private void ParallelReject(String user) + { + List tasks = taskComponent.getAssignedTasks(user, WorkflowTaskState.IN_PROGRESS); + assertEquals(1, tasks.size()); + WorkflowTask task = tasks.get(0); + taskComponent.endTask(task.getId(), "reject"); + } + public void testSignal() { Map parameters = new HashMap(); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); WorkflowDefinition workflowDef = getTestDefinition(); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); @@ -365,7 +440,7 @@ public class JBPMEngineTest extends BaseSpringTest WorkflowDefinition workflowDef = getTestDefinition(); Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), AuthenticationUtil.getAdminUserName()); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); @@ -395,7 +470,7 @@ public class JBPMEngineTest extends BaseSpringTest bpm_assignees.add("fred"); Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "assignees"), (Serializable)bpm_assignees); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); List tasks = workflowComponent.getTasksForWorkflowPath(path.id); @@ -411,7 +486,7 @@ public class JBPMEngineTest extends BaseSpringTest WorkflowDefinition workflowDef = getTestDefinition(); Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), AuthenticationUtil.getAdminUserName()); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); @@ -435,7 +510,7 @@ public class JBPMEngineTest extends BaseSpringTest WorkflowDefinition workflowDef = getTestDefinition(); Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), AuthenticationUtil.getAdminUserName()); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); assertNotNull(path); @@ -453,7 +528,7 @@ public class JBPMEngineTest extends BaseSpringTest WorkflowDefinition workflowDef = getTestDefinition(); Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), AuthenticationUtil.getAdminUserName()); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); @@ -477,7 +552,7 @@ public class JBPMEngineTest extends BaseSpringTest WorkflowDefinition workflowDef = deployment.definition; Map parameters = new HashMap(); - parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), rootNodeRef); parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); @@ -500,6 +575,29 @@ public class JBPMEngineTest extends BaseSpringTest return testWorkflowDef; } + + private NodeRef createPerson(String userName) + { + // if user with given user name doesn't already exist then create user + if (this.authenticationService.authenticationExists(userName) == false) + { + // create user + this.authenticationService.createAuthentication(userName, "password".toCharArray()); + } + + // if person node with given user name doesn't already exist then create + // person + if (this.personService.personExists(userName) == false) + { + // create person properties + PropertyMap personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, userName); + + // create person node for user + return personService.createPerson(personProps); + } + return personService.getPerson(userName); + } /** * Filter task list by workflow instance