diff --git a/config/alfresco/extension/workflow-context.xml.sample b/config/alfresco/extension/workflow-context.xml.sample new file mode 100644 index 0000000000..f1e087c9f3 --- /dev/null +++ b/config/alfresco/extension/workflow-context.xml.sample @@ -0,0 +1,33 @@ + + + + + + + + + + jbpm + alfresco/workflow/parallelreview_processdefinition.xml + text/xml + false + + + + + + + + + + + + + + + + + + + + diff --git a/config/alfresco/workflow/adhoc_processdefinition.xml b/config/alfresco/workflow/adhoc_processdefinition.xml index 54db1cd748..101ceaf168 100644 --- a/config/alfresco/workflow/adhoc_processdefinition.xml +++ b/config/alfresco/workflow/adhoc_processdefinition.xml @@ -17,14 +17,8 @@ diff --git a/config/alfresco/workflow/parallelreview_processdefinition.xml b/config/alfresco/workflow/parallelreview_processdefinition.xml new file mode 100644 index 0000000000..2ccbae432b --- /dev/null +++ b/config/alfresco/workflow/parallelreview_processdefinition.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + #{bpm_assignees} + reviewer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #{wf_actualPercent >= wf_requiredApprovePercent} + + + + + + + + + + + + + + + + diff --git a/config/alfresco/workflow/review_processdefinition.xml b/config/alfresco/workflow/review_processdefinition.xml index a3a9403ccf..e26c57d889 100644 --- a/config/alfresco/workflow/review_processdefinition.xml +++ b/config/alfresco/workflow/review_processdefinition.xml @@ -17,14 +17,8 @@ diff --git a/config/alfresco/workflow/workflow-messages.properties b/config/alfresco/workflow/workflow-messages.properties index c21271e212..32aba2a288 100644 --- a/config/alfresco/workflow/workflow-messages.properties +++ b/config/alfresco/workflow/workflow-messages.properties @@ -35,6 +35,41 @@ wf_review.task.wf_approvedTask.description=Approved wf_review.node.end.title=End wf_review.node.end.description=End + +# +# Parallel Review Workflow +# + +wf_parallelreview.workflow.title=Group Review & Approve +wf_parallelreview.workflow.description=Group Review & approval of content + +# Parallel Review & Approve Task Definitions + +wf_workflowmodel.type.wf_submitParallelReviewTask.title=Start Group Review +wf_workflowmodel.type.wf_submitParallelReviewTask.description=Submit documents for review & approval to a group of people +wf_workflowmodel.property.wf_requiredApprovePercent.title=Required approval percentage +wf_workflowmodel.property.wf_requiredApprovePercent.description=Percentage of reviewers who must approve for approval +wf_workflowmodel.type.wf_rejectedParallelTask.title=Rejected +wf_workflowmodel.type.wf_rejectedParallelTask.description=Rejected +wf_workflowmodel.type.wf_approvedParallelTask.title=Approved +wf_workflowmodel.type.wf_approvedParallelTask.description=Approved +wf_workflowmodel.property.wf_reviewerCount.title=Number of reviewers +wf_workflowmodel.property.wf_reviewerCount.description=Number of reviewers +wf_workflowmodel.property.wf_requiredPercent.title=Required approval percentage +wf_workflowmodel.property.wf_requiredPercent.description=Required approval percentage +wf_workflowmodel.property.wf_approveCount.title=Reviewers who approved +wf_workflowmodel.property.wf_approveCount.description=Reviewers who approved +wf_workflowmodel.property.wf_actualPercent.title=Actual approval percentage +wf_workflowmodel.property.wf_actualPercentdescription=Actual approval percentage + +# Group Review & Approve Process Definitions + +wf_parallelreview.node.review.transition.reject.title=Reject +wf_parallelreview.node.review.transition.reject.description=Reject +wf_parallelreview.node.review.transition.approve.title=Approve +wf_parallelreview.node.review.transition.approve.description=Approve + + # # Adhoc Task Workflow # diff --git a/config/alfresco/workflow/workflowModel.xml b/config/alfresco/workflow/workflowModel.xml index 015c704708..13224b1cf4 100644 --- a/config/alfresco/workflow/workflowModel.xml +++ b/config/alfresco/workflow/workflowModel.xml @@ -23,6 +23,26 @@ bpm:assignee + + + bpm:startTask + + + d:int + true + 50 + + + 1 + 100 + + + + + + bpm:assignees + + bpm:workflowTask @@ -32,7 +52,21 @@ + + + bpm:workflowTask + + wf:parallelReviewStats + + + + bpm:workflowTask + + wf:parallelReviewStats + + + @@ -84,6 +118,27 @@ - + + + + + + + + d:int + + + d:int + + + d:int + + + d:int + + + + + \ No newline at end of file diff --git a/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java index 88679c8642..f16e0e14b8 100644 --- a/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java +++ b/source/java/org/alfresco/repo/workflow/WorkflowInterpreter.java @@ -380,10 +380,10 @@ public class WorkflowInterpreter out.println("description: " + task.description); out.println("state: " + task.state); out.println("path: " + task.path.id); - out.println("transitions: " + task.path.node.transitions.length); - for (WorkflowTransition transition : task.path.node.transitions) + out.println("transitions: " + task.definition.node.transitions.length); + for (WorkflowTransition transition : task.definition.node.transitions) { - out.println(" transition: " + ((transition == null || transition.id.equals("")) ? "[default]" : transition.id) + " , title: " + transition.title + " , desc: " + transition.description); + out.println(" transition: " + ((transition.id == null || transition.id.equals("")) ? "[default]" : transition.id) + " , title: " + transition.title + " , desc: " + transition.description); } out.println("properties: " + task.properties.size()); for (Map.Entry prop : task.properties.entrySet()) @@ -430,7 +430,7 @@ public class WorkflowInterpreter } out.println("deployed definition id: " + def.id + " , name: " + def.name + " , title: " + def.title + " , version: " + def.version); currentDeploy = command[1]; - out.print(interpretCommand("use " + def.id)); + out.print(interpretCommand("use definition " + def.id)); } else if (command[0].equals("redeploy")) @@ -520,6 +520,10 @@ public class WorkflowInterpreter return "Syntax Error.\n"; } } + if (currentWorkflowDef == null) + { + return "Workflow definition not selected.\n"; + } WorkflowPath path = workflowService.startWorkflow(currentWorkflowDef.id, params); out.println("started workflow id: " + path.instance.id + ", path: " + path.id + " , node: " + path.node.name + " , def: " + path.instance.definition.title); currentPath = path; diff --git a/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java new file mode 100644 index 0000000000..c9569ccfcb --- /dev/null +++ b/source/java/org/alfresco/repo/workflow/jbpm/ForEachFork.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.alfresco.repo.workflow.jbpm; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.alfresco.service.cmr.workflow.WorkflowException; +import org.dom4j.Element; +import org.jbpm.graph.def.ActionHandler; +import org.jbpm.graph.def.Node; +import org.jbpm.graph.def.Transition; +import org.jbpm.graph.exe.ExecutionContext; +import org.jbpm.graph.exe.Token; +import org.jbpm.instantiation.FieldInstantiator; +import org.jbpm.jpdl.el.impl.JbpmExpressionEvaluator; + + +/** + * For each "item in collection", create a fork. + */ +public class ForEachFork implements ActionHandler +{ + private static final long serialVersionUID = 4643103713602441652L; + + private Element foreach; + private String var; + + + /** + * Create a new child token for each item in list. + * + * @param executionContext + * @throws Exception + */ + @SuppressWarnings("unchecked") + public void execute(final ExecutionContext executionContext) + throws Exception + { + // + // process action handler arguments + // + + if (foreach == null) + { + 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("#{")) + { + Object eval = JbpmExpressionEvaluator.evaluate(forEachCollStr, executionContext); + 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 collection + else if (eval instanceof Collection) + { + forEachColl = (List)eval; + } + } + } + else + { + forEachColl = (List)FieldInstantiator.getValue(List.class, foreach); + } + + if (var == null || var.length() == 0) + { + throw new WorkflowException("forEach variable name has not been provided"); + } + + // + // 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++) + { + Transition transition = (Transition) node.getLeavingTransitions().get(i); + + for (int iVar = 0; iVar < forEachColl.size(); iVar++) + { + // 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); + + // assign variable within path + final ExecutionContext newExecutionContext = new ExecutionContext(loopToken); + newExecutionContext.getContextInstance().createVariable(var, forEachColl.get(iVar), loopToken); + + // record path & transition + ForkedTransition forkTransition = new ForkedTransition(); + forkTransition.executionContext = newExecutionContext; + forkTransition.transition = transition; + forkTransitions.add(forkTransition); + } + } + + // + // let each new token leave the node. + // + for (ForkedTransition forkTransition : forkTransitions) + { + node.leave(forkTransition.executionContext, forkTransition.transition); + } + } + + /** + * Create a token name + * + * @param parent + * @param transitionName + * @return + */ + protected String getTokenName(Token parent, String transitionName, int loopIndex) + { + String tokenName = null; + if (transitionName != null) + { + 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 + int size = ( parent.getChildren()!=null ? parent.getChildren().size()+1 : 1 ); + tokenName = Integer.toString(size); + } + return tokenName + "." + loopIndex; + } + + /** + * Fork Transition + */ + private class ForkedTransition + { + private ExecutionContext executionContext; + private Transition transition; + } + +} diff --git a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java index 74b584a8d4..70932b3a91 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngine.java @@ -68,6 +68,7 @@ import org.hibernate.proxy.HibernateProxy; import org.jbpm.JbpmContext; import org.jbpm.JbpmException; import org.jbpm.context.exe.ContextInstance; +import org.jbpm.context.exe.TokenVariableMap; import org.jbpm.db.GraphSession; import org.jbpm.db.TaskMgmtSession; import org.jbpm.graph.def.Node; @@ -909,6 +910,18 @@ public class JBPMEngine extends BPMEngine TaskMgmtSession taskSession = context.getTaskMgmtSession(); TaskInstance taskInstance = getTaskInstance(taskSession, taskId); + // ensure all mandatory properties have been provided + QName[] missingProps = getMissingMandatoryTaskProperties(taskInstance); + if (missingProps != null && missingProps.length > 0) + { + String props = ""; + for (int i = 0; i < missingProps.length; i++) + { + props += missingProps[i].toString() + ((i < missingProps.length -1) ? "," : ""); + } + throw new WorkflowException("Mandatory task properties have not been provided: " + props); + } + // signal the transition on the task if (transition == null) { @@ -1190,10 +1203,32 @@ public class JBPMEngine extends BPMEngine Map taskProperties = taskDef.getProperties(); Map taskAssocs = taskDef.getAssociations(); + // build properties from jBPM context (visit all tokens to the root) + Map vars = instance.getVariablesLocally(); + if (!localProperties) + { + ContextInstance context = instance.getContextInstance(); + Token token = instance.getToken(); + while (token != null) + { + TokenVariableMap varMap = context.getTokenVariableMap(token); + if (varMap != null) + { + Map tokenVars = varMap.getVariablesLocally(); + for (Map.Entry entry : tokenVars.entrySet()) + { + if (!vars.containsKey(entry.getKey())) + { + vars.put(entry.getKey(), entry.getValue()); + } + } + } + token = token.getParent(); + } + } + // map arbitrary task variables Map properties = new HashMap(10); - Map vars = (localProperties ? instance.getVariablesLocally() : instance.getVariables()); - for (Entry entry : vars.entrySet()) { String key = entry.getKey(); @@ -1544,6 +1579,69 @@ public class JBPMEngine extends BPMEngine } } + /** + * Get missing mandatory properties on Task + * + * @param instance task instance + * @return array of missing property names (or null, if none) + */ + protected QName[] getMissingMandatoryTaskProperties(TaskInstance instance) + { + List missingProps = null; + + // retrieve properties of task + Map existingValues = getTaskProperties(instance, false); + + // retrieve definition of task + ClassDefinition classDef = getAnonymousTaskDefinition(getTaskDefinition(instance.getTask())); + Map propertyDefs = classDef.getProperties(); + Map assocDefs = classDef.getAssociations(); + + // for each property, determine if it is mandatory + for (Map.Entry entry : propertyDefs.entrySet()) + { + QName name = entry.getKey(); + if (!(name.getNamespaceURI().equals(NamespaceService.CONTENT_MODEL_1_0_URI) || (name.getNamespaceURI().equals(NamespaceService.SYSTEM_MODEL_1_0_URI)))) + { + boolean isMandatory = entry.getValue().isMandatory(); + if (isMandatory) + { + Object value = existingValues.get(entry.getKey()); + if (value == null || (value instanceof String && ((String)value).length() == 0)) + { + if (missingProps == null) + { + missingProps = new ArrayList(); + } + missingProps.add(entry.getKey()); + } + } + } + } + for (Map.Entry entry : assocDefs.entrySet()) + { + QName name = entry.getKey(); + if (!(name.getNamespaceURI().equals(NamespaceService.CONTENT_MODEL_1_0_URI) || (name.getNamespaceURI().equals(NamespaceService.SYSTEM_MODEL_1_0_URI)))) + { + boolean isMandatory = entry.getValue().isTargetMandatory(); + if (isMandatory) + { + Object value = existingValues.get(entry.getKey()); + if (value == null || (value instanceof List && ((List)value).size() == 0)) + { + if (missingProps == null) + { + missingProps = new ArrayList(); + } + missingProps.add(entry.getKey()); + } + } + } + } + + return (missingProps == null) ? null : missingProps.toArray(new QName[missingProps.size()]); + } + /** * Convert a Repository association to JBPMNodeList or JBPMNode * diff --git a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java index 47c9465e0d..f85d55f7c4 100644 --- a/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java +++ b/source/java/org/alfresco/repo/workflow/jbpm/JBPMEngineTest.java @@ -32,6 +32,7 @@ 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; @@ -60,6 +61,7 @@ public class JBPMEngineTest extends BaseSpringTest NodeService nodeService; WorkflowComponent workflowComponent; TaskComponent taskComponent; + WorkflowPackageComponent packageComponent; WorkflowDefinition testWorkflowDef; NodeRef testNodeRef; @@ -74,6 +76,7 @@ public class JBPMEngineTest extends BaseSpringTest BPMEngineRegistry registry = (BPMEngineRegistry)applicationContext.getBean("bpm_engineRegistry"); workflowComponent = registry.getWorkflowComponent("jbpm"); taskComponent = registry.getTaskComponent("jbpm"); + packageComponent = (WorkflowPackageComponent)applicationContext.getBean("workflowPackageImpl"); // deploy test process messages I18NUtil.registerResourceBundle("org/alfresco/repo/workflow/jbpm/test-messages"); @@ -357,6 +360,7 @@ public class JBPMEngineTest extends BaseSpringTest Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), "admin"); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); List tasks = workflowComponent.getTasksForWorkflowPath(path.id); @@ -402,6 +406,7 @@ public class JBPMEngineTest extends BaseSpringTest Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), "admin"); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); List tasks1 = workflowComponent.getTasksForWorkflowPath(path.id); @@ -443,6 +448,7 @@ public class JBPMEngineTest extends BaseSpringTest Map parameters = new HashMap(); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "reviewer"), "admin"); parameters.put(QName.createQName(NamespaceService.DEFAULT_URI, "testNode"), testNodeRef); + parameters.put(QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); List tasks1 = workflowComponent.getTasksForWorkflowPath(path.id); @@ -466,6 +472,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.BPM_MODEL_1_0_URI, "package"), packageComponent.createPackage(null)); WorkflowPath path = workflowComponent.startWorkflow(workflowDef.id, parameters); assertNotNull(path); List tasks1 = workflowComponent.getTasksForWorkflowPath(path.id);