diff --git a/imixs-workflow-core/src/main/java/org/imixs/workflow/bpmn/BPMNModelHandler.java b/imixs-workflow-core/src/main/java/org/imixs/workflow/bpmn/BPMNModelHandler.java index a1d69e444..5043f510d 100644 --- a/imixs-workflow-core/src/main/java/org/imixs/workflow/bpmn/BPMNModelHandler.java +++ b/imixs-workflow-core/src/main/java/org/imixs/workflow/bpmn/BPMNModelHandler.java @@ -49,6 +49,10 @@ public class BPMNModelHandler extends DefaultHandler { boolean bLinkCatchEvent = false; boolean bItemValue = false; boolean bdocumentation = false; + boolean bSequenceFlow = false; + + boolean bconditionExpression = false; + ItemCollection currentEntity = null; String currentItemName = null; String currentItemType = null; @@ -56,6 +60,7 @@ public class BPMNModelHandler extends DefaultHandler { String currentMessageName = null; String currentAnnotationName = null; String currentLinkName = null; + String bpmnID = null; StringBuilder characterStream = null; @@ -72,7 +77,10 @@ public class BPMNModelHandler extends DefaultHandler { Map messageCache = null; Map annotationCache = null; + Map conditionCache = null; + List startEvents = null; + List conditionalGatewayCache = null; ItemCollection definition = null; @@ -86,10 +94,13 @@ public BPMNModelHandler() { eventCache = new HashMap(); messageCache = new HashMap(); annotationCache = new HashMap(); + conditionCache = new HashMap(); linkThrowEventCache = new HashMap(); linkCatchEventCache = new HashMap(); + conditionalGatewayCache = new ArrayList(); + startEvents = new ArrayList(); sequenceCache = new HashMap(); @@ -201,6 +212,7 @@ public void startElement(String uri, String localName, String qName, Attributes // bpmn2:sequenceFlow - cache all sequenceFlows... if (qName.equalsIgnoreCase("bpmn2:sequenceFlow")) { bpmnID = attributes.getValue("id"); + bSequenceFlow = true; String source = attributes.getValue("sourceRef"); String target = attributes.getValue("targetRef"); sequenceCache.put(bpmnID, new SequenceFlow(source, target)); @@ -245,6 +257,18 @@ public void startElement(String uri, String localName, String qName, Attributes currentMessageName = attributes.getValue("name"); } + if (qName.equalsIgnoreCase("bpmn2:exclusiveGateway") || qName.equalsIgnoreCase("bpmn2:inclusiveGateway") + || qName.equalsIgnoreCase("bpmn2:eventBasedGateway")) { + // Put conditona Gateway ID into th gateway cache... + conditionalGatewayCache.add(attributes.getValue("id")); + } + + // test for conditional Expression... + if (qName.equalsIgnoreCase("bpmn2:conditionExpression")) { + bconditionExpression = true; + characterStream = new StringBuilder(); + } + if (qName.equalsIgnoreCase("bpmn2:textAnnotation")) { bAnnotation = true; currentAnnotationName = attributes.getValue("id"); @@ -346,13 +370,21 @@ public void endElement(String uri, String localName, String qName) throws SAXExc linkCatchEventCache.put(currentLinkName, bpmnID); } + // test conditional sequence flow... + if (bSequenceFlow && bconditionExpression && qName.equalsIgnoreCase("bpmn2:conditionExpression")) { + String svalue = characterStream.toString(); + logger.fine("conditional SequenceFlow:" + bpmnID + "=" + svalue); + bconditionExpression = false; + conditionCache.put(bpmnID, svalue); + } + } // @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public void characters(char ch[], int start, int length) throws SAXException { - if (bdocumentation) { + if (bdocumentation || bconditionExpression) { characterStream = characterStream.append(new String(ch, start, length)); } @@ -367,19 +399,19 @@ public void characters(char ch[], int start, int length) throws SAXException { /** * This method builds the model from the information parsed by the handler. - * First all task elements were adds as unique process entities into the - * model. In the second step the method adds the Activity elements to the - * assigned Task. We look also for activities with no incoming SequenceFlow. + * First all task elements were adds as unique process entities into the model. + * In the second step the method adds the Activity elements to the assigned + * Task. We look also for activities with no incoming SequenceFlow. * - * The builder verifies the ProcessIDs for each task element to guaranty - * that the numProcessID is unique + * The builder verifies the ProcessIDs for each task element to guaranty that + * the numProcessID is unique * * The build connects pairs of Catch and Throw LinkEvents with a virtual * SequenceFlow to support the same behavior as if those elements where * connected directly in the model. * - * The method tests the model for bpmn2:message elements and replace links - * in Activity elements attribute 'rtfMailBody' + * The method tests the model for bpmn2:message elements and replace links in + * Activity elements attribute 'rtfMailBody' * * @throws ModelException */ @@ -487,13 +519,13 @@ private String getAnnotationForElement(String elementID) { } /** - * This method returns all SourceTask Elements connected to a given eventID. - * The method takes care about loop events and follow up events. Later ones - * are handled by the method addImixsEvent(). For that reason, the result of - * this method can be also an empty list. + * This method returns all SourceTask Elements connected to a given eventID. The + * method takes care about loop events and follow up events. Later ones are + * handled by the method addImixsEvent(). For that reason, the result of this + * method can be also an empty list. * - * An event can be a shared event so it is possible that more than one - * source tasks are found + * An event can be a shared event so it is possible that more than one source + * tasks are found * * @param eventID * @throws ModelException @@ -578,16 +610,16 @@ private List findSourceTasks(String eventID) throws ModelExcepti } /** - * This method computes the target for an event and adds the event to a - * source task. The method call recursive if the target is a followUp Event. + * This method computes the target for an event and adds the event to a source + * task. The method call recursive if the target is a followUp Event. * * If a event has no target the method throws an exception * - * If a event has more than one targets (task or event elements) then the - * event is handled as a loop event. + * If a event has more than one targets (task or event elements) then the event + * is handled as a loop event. * - * If a event is already assigned to the sourceTask, the method returns - * without adding the event. + * If a event is already assigned to the sourceTask, the method returns without + * adding the event. * * @param sourceTask * @param event @@ -644,6 +676,36 @@ private void addImixsEvent(String eventID, ItemCollection sourceTask) throws Mod event.removeItem("keyFollowUp"); event.replaceItemValue("numNextProcessID", sourceTask.getItemValue("numProcessID")); + // test if this is a conditional event - search for gateway... + List outgoingList = this.findOutgoingFlows(eventID); + if (outgoingList != null && outgoingList.size() > 0) { + Map conditions=new HashMap(); + for (SequenceFlow flow : outgoingList) { + if (conditionalGatewayCache.contains(flow.target)) { + + String conditionalGatewayID = flow.target; + // get all outgoing flows from this gateway + List conditionalFlows = this.findOutgoingFlows(conditionalGatewayID); + for (SequenceFlow condFlow : conditionalFlows) { + ItemCollection targetTask = new ElementResolver().findImixsTargetTask(condFlow); + // build the condition + if (targetTask != null) { + String sExpression = findConditionBySquenceFlow(condFlow); + logger.fine("add condition: " +targetTask.getItemValueInteger("numProcessid") + "="+ sExpression); + conditions.put(targetTask.getItemValueInteger("numProcessid"), sExpression); + } + } + + } + + } + // add the attribute 'keyConditions' if available... + if (!conditions.isEmpty()) { + event.replaceItemValue("keyConditions", conditions); + } + + } + // here we need to check if one of the targets is an event - this // need to be handled in a recursive call for (String elementID : targetList) { @@ -673,9 +735,7 @@ private void addImixsEvent(String eventID, ItemCollection sourceTask) throws Mod if (followUpEventID != null) { // recursive call! addImixsEvent(followUpEventID, sourceTask); - ItemCollection followUpEvent = eventCache.get(followUpEventID); - event.replaceItemValue("keyFollowUp", "1"); event.replaceItemValue("numNextActivityID", followUpEvent.getItemValue("numactivityid")); @@ -709,9 +769,8 @@ private void addImixsEvent(String eventID, ItemCollection sourceTask) throws Mod } /** - * This helper method verifies if the activity of the event is still unique - * for the task element. If not the method computes a new one and updates - * the event + * This helper method verifies if the activity of the event is still unique for + * the task element. If not the method computes a new one and updates the event * * @param event * @param task @@ -788,8 +847,7 @@ private List findOutgoingFlows(String elementID) { } /** - * This method returns all incoming Associations flows for a given element - * ID + * This method returns all incoming Associations flows for a given element ID * * @param elementID * @return @@ -808,8 +866,7 @@ private List findIncomingAssociations(String elementID) { } /** - * This method returns all outgoing Associations flows for a given element - * ID + * This method returns all outgoing Associations flows for a given element ID * * @param elementID * @return @@ -826,6 +883,30 @@ private List findOutgoingAssociations(String elementID) { return result; } + /** + * This method returns an optional condition for a given sequenceFlow object. + * The method iterates the conditionCache to lookup the condition + * + * @param flow + * @return the condition if available or null + */ + private String findConditionBySquenceFlow(SequenceFlow flow) { + if (conditionCache == null) { + return null; + } + // first we need do figure out the squenceFlowID for the flow object + String sequenceID = null; + for (Map.Entry entry : sequenceCache.entrySet()) { + String key = entry.getKey(); + SequenceFlow value = entry.getValue(); + if (value == flow) { + sequenceID = key; + break; + } + } + return conditionCache.get(sequenceID); + } + /** * This method parses an event for the text fragment * ... and replaces the tag with the @@ -877,9 +958,8 @@ public SequenceFlow(String source, String target) { } /** - * This helper class provides methods to resolve the connected Imixs - * elements to a flow element. The constructor is used to initialize a - * loopDetection cache + * This helper class provides methods to resolve the connected Imixs elements to + * a flow element. The constructor is used to initialize a loopDetection cache * * @author rsoika * @@ -893,9 +973,9 @@ public ElementResolver() { } /** - * This method searches a Imixs Task Element connected to the given - * SequenceFlow element. If the Sequence Flow is not connected to a - * Imixs Task element the method returns null. + * This method searches a Imixs Task Element connected to the given SequenceFlow + * element. If the Sequence Flow is not connected to a Imixs Task element the + * method returns null. * * * @return the Imixs Task element or null if no Task Element was found. @@ -943,12 +1023,11 @@ public List findAllImixsSourceTasks(SequenceFlow flow, List findAllImixsTargetIDs(SequenceFlow flow, List targetList) { @@ -1133,12 +1212,12 @@ public List findAllImixsTargetIDs(SequenceFlow flow, List target } /** - * This method searches for all target tasks for a outgoing sequence - * flow. The method returns a List of possible imixs task elements. + * This method searches for all target tasks for a outgoing sequence flow. The + * method returns a List of possible imixs task elements. * * - * @return the ID of the Imixs Event element or null if no Event Element - * was found. + * @return the ID of the Imixs Event element or null if no Event Element was + * found. * @return */ public List findAllImixsTargetTaskIDs(SequenceFlow flow, List targetList) { diff --git a/imixs-workflow-core/src/test/java/org/imixs/workflow/bpmn/TestBPMNParserConditionalEvents.java b/imixs-workflow-core/src/test/java/org/imixs/workflow/bpmn/TestBPMNParserConditionalEvents.java index 6b1713d7f..4fdc97e5f 100644 --- a/imixs-workflow-core/src/test/java/org/imixs/workflow/bpmn/TestBPMNParserConditionalEvents.java +++ b/imixs-workflow-core/src/test/java/org/imixs/workflow/bpmn/TestBPMNParserConditionalEvents.java @@ -5,6 +5,7 @@ import java.io.UnsupportedEncodingException; import java.text.ParseException; import java.util.List; +import java.util.Map; import javax.xml.parsers.ParserConfigurationException; @@ -14,6 +15,7 @@ import org.imixs.workflow.exceptions.ModelException; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.xml.sax.SAXException; @@ -37,11 +39,12 @@ public void teardown() { } + @SuppressWarnings("unchecked") @Test public void testSimple() throws ParseException, ParserConfigurationException, SAXException, IOException, ModelException { - InputStream inputStream = getClass().getResourceAsStream("/bpmn/conditional_event.bpmn"); + InputStream inputStream = getClass().getResourceAsStream("/bpmn/conditional_event1.bpmn"); BPMNModel model = null; try { @@ -78,9 +81,67 @@ public void testSimple() Assert.assertEquals(1000, activity.getItemValueInteger("numNextProcessID")); - // TODO // Now we need to evaluate if the Event is marked as an conditional Event with // the condition list copied from the gateway. + Assert.assertTrue(activity.hasItem("keyConditions")); + Map conditions=(Map) activity.getItemValue("keyConditions").get(0); + Assert.assertNotNull(conditions); + Assert.assertEquals("(workitem._budget && workitem._budget[0]>100)", conditions.get(1100)); + Assert.assertEquals("(workitem._budget && workitem._budget[0]<=100)", conditions.get(1200)); + } + + + @SuppressWarnings("unchecked") + @Test + @Ignore + public void testFollowUp() + throws ParseException, ParserConfigurationException, SAXException, IOException, ModelException { + + InputStream inputStream = getClass().getResourceAsStream("/bpmn/conditional_event2.bpmn"); + + BPMNModel model = null; + try { + model = BPMNParser.parseModel(inputStream, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + Assert.fail(); + } catch (ModelException e) { + e.printStackTrace(); + Assert.fail(); + } + Assert.assertNotNull(model); + + // Test Environment + ItemCollection profile = model.getDefinition(); + Assert.assertNotNull(profile); + + // test count of elements + Assert.assertEquals(3, model.findAllTasks().size()); + + // test task 1000 + ItemCollection task = model.getTask(1000); + Assert.assertNotNull(task); + + // test events for task 1000 + List events = model.findAllEventsByTask(1000); + Assert.assertNotNull(events); + Assert.assertEquals(2, events.size()); + + // test activity 1000.10 submit + ItemCollection activity = model.getEvent(1000, 10); + Assert.assertNotNull(activity); + Assert.assertEquals("conditional event", activity.getItemValueString("txtname")); + + Assert.assertEquals(1000, activity.getItemValueInteger("numNextProcessID")); + + + // Now we need to evaluate if the Event is marked as an conditional Event with + // the condition list copied from the gateway. + Assert.assertTrue(activity.hasItem("keyConditions")); + Map conditions=(Map) activity.getItemValue("keyConditions").get(0); + Assert.assertNotNull(conditions); + Assert.assertEquals("(workitem._budget && workitem._budget[0]>100)", conditions.get(1100)); + Assert.assertEquals("(workitem._budget && workitem._budget[0]<=100)", conditions.get(1200)); } diff --git a/imixs-workflow-core/src/test/resources/bpmn/conditional_event.bpmn b/imixs-workflow-core/src/test/resources/bpmn/conditional_event1.bpmn similarity index 100% rename from imixs-workflow-core/src/test/resources/bpmn/conditional_event.bpmn rename to imixs-workflow-core/src/test/resources/bpmn/conditional_event1.bpmn diff --git a/imixs-workflow-core/src/test/resources/bpmn/conditional_event2.bpmn b/imixs-workflow-core/src/test/resources/bpmn/conditional_event2.bpmn new file mode 100644 index 000000000..849551470 --- /dev/null +++ b/imixs-workflow-core/src/test/resources/bpmn/conditional_event2.bpmn @@ -0,0 +1,160 @@ + + + + + + + + + + + SequenceFlow_1 + + + SequenceFlow_11 + SequenceFlow_12 + + + SequenceFlow_1 + SequenceFlow_4 + + + SequenceFlow_7 + SequenceFlow_11 + + + SequenceFlow_3 + SequenceFlow_12 + + + SequenceFlow_4 + SequenceFlow_5 + + + SequenceFlow_5 + SequenceFlow_7 + SequenceFlow_2 + + + + + + (workitem._budget && workitem._budget[0]>100) + + + + + SequenceFlow_2 + SequenceFlow_3 + + + (workitem._budget && workitem._budget[0]<=100) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file