CMMN Listener Execution to achieve CMMN "Multiple Instance"?

Hey guys.

I tried a couple of workarounds with CaseExecutionListeners today and are a bit puzzled how to achieve the following:

  @Test
  @Deployment(resources = "test.cmmn")
  public void testRepeatingSentry() {    
    CaseService caseService = processEngine().getCaseService();
    TaskService taskService = processEngine().getTaskService();

    CaseInstance ci = caseService.createCaseInstanceByKey( //
        "test", //
        Variables.createVariables() //
          .putValue("a", 5));
    
    assertEquals(2, caseService.createCaseExecutionQuery().caseInstanceId(ci.getId()).activityId("HumanTask_Test").active().count() );
    assertEquals(1, caseService.createCaseExecutionQuery().caseInstanceId(ci.getId()).activityId("HumanTask_Test").available().count() );
    assertEquals(2, taskService.createTaskQuery().caseInstanceId(ci.getCaseInstanceId()).count() );
    
  }

This is my CMMN Model: test.cmmn (4.4 KB)

I have the listener on the Human Task and want to trigger basically a second instance of the Human Task via setting a variable and using the VariableOnPart-Sentry. As the Human Task is repeatable, that should be possible (I thought).

But when I attach the listener to the event “start”, it seems not to trigger the VariableOnPart at all.
When I attach the listener to the event “create”, it does an endless loop, as it checks all incoming sentries (even the “normal” one which just fired) and creates the current task again - and again - and again… Easily to test with the test case I provided. Here the listener (hey - I cannot attach Java Code to the forum?):

public class MultipleInstanceTaskStartListener implements CaseExecutionListener {

  @Override
  public void notify(final DelegateCaseExecution ctx) throws Exception {
    if (!ctx.hasVariable("elementTrigger")) {
      // First element
      ctx.setVariable(ctx.getActivityId() + "_started", true);
      ctx.setVariable("elementTrigger", null);
      
      Collection elements = (Collection) ctx.getVariable("elements");
      Iterator iterator = elements.iterator();
      Object firstElement = iterator.next();
      ctx.setVariableLocal("element", firstElement);
      
      iterator.forEachRemaining(new Consumer() {
        public void accept(Object element) {
          ctx.setVariable("elementTrigger", element);        
        }
      });
      
    } else {
      // next elements
      Object element = ctx.getVariable("elementTrigger");
      ctx.setVariableLocal("element", element);
    }
  }

Understandable what I mean?

I wonder if this is somehow possible? Or what could be a good workaround to achieve what I want?

The real use case behind is a kind of “Multiple Instance” task which I have to create in parallel for every document in a list of documents. I need the first “Normal” sentry in order to avoid auto-completion and the VariuableOnPart to trigger a arbritrary amount of additional tasks later on.

I also thought about

  • using a list as variable
  • use this in the “normal” sentry if part
  • add a “create” listener which checks the list, removes the first element (as this is currently started) and set the list
  • this triggers a new evaluation and a second task
  • until the list is empty.

HEre is the CMMN: MultipleInstance2.cmmn (3.0 KB), and the listener:

public class MultipleInstance2TaskCreateListener implements CaseExecutionListener {

  @Override
  public void notify(final DelegateCaseExecution ctx) throws Exception {
     
      Collection elements = (Collection) ctx.getVariable("elements");
      if (elements.iterator().hasNext()) {
        Object firstElement = elements.iterator().next();
        
        elements.remove(firstElement);
        ctx.setVariableLocal("element", firstElement);
        ctx.setVariable("elements", elements);
      }      
  }

But this seems to change the case state itself - so it produces an exception:

Caused by: org.camunda.bpm.engine.exception.cmmn.CaseIllegalStateTransitionException: ENGINE-05010 Could not perform transition 'create on case execution with id '18'.Reason: The case execution is already in state 'available'.
	at org.camunda.bpm.engine.impl.cmmn.behavior.CmmnBehaviorLogger.isAlreadyInStateException(CmmnBehaviorLogger.java:165)
	at org.camunda.bpm.engine.impl.cmmn.behavior.PlanItemDefinitionActivityBehavior.ensureTransitionAllowed(PlanItemDefinitionActivityBehavior.java:226)
	at org.camunda.bpm.engine.impl.cmmn.behavior.PlanItemDefinitionActivityBehavior.onCreate(PlanItemDefinitionActivityBehavior.java:90)

So I am a but puzzled which way WOULD be a possible to do this in plain CMMN. Any idea?

Cheers
Bernd

Hi @BerndRuecker,

I debugged the MultipleInstance2.cmmn and the MultipleInstance2TastCreateListener. The problem is, when the variable elements is set inside the listener then the entry criterion of the human task is triggered. The triggered entry criterion starts the human task and this breaks the loop of executing the create listeners of the human task. As a consequence, the engine tries to perform the create transition for the human task once again.

Maybe you could try this approach MultipleInstance2.cmmn (1.6 KB) and with that implementation of the listener:

public class MyCaseExecutionListener implements CaseExecutionListener {

  @Override
  public void notify(DelegateCaseExecution ctx) throws Exception {
    Collection elements = (Collection) ctx.getVariable("elements");

    Iterator iterator = elements.iterator();

    if (iterator.hasNext()) {
      Object firstElement = iterator.next();
      ctx.setVariableLocal("element", firstElement);
    }

    CaseExecutionEntity parent = ((CaseExecutionEntity) ctx).getParent();
    List<CmmnExecution> children = new ArrayList<CmmnExecution>();
    CmmnActivity activity = ((CaseExecutionEntity) ctx).getActivity();

    while (iterator.hasNext()) {
      Object value = iterator.next();

      List<CmmnExecution> tmp = parent.createChildExecutions(Arrays.asList(activity));
      tmp.get(0).setVariableLocal("element", value);

      children.addAll(tmp);
    }

    if (!children.isEmpty()) {
      ctx.setVariable("elements", new ArrayList());
      parent.triggerChildExecutionsLifecycle(children);
    }

  }

}

Does it help you?

Cheers,
Roman

Hi Roman.

Thanks for the quick response. This does indeed work and I think I can adjust it to the real use case. Great :slight_smile: The downside is, that it uses internal API. Just to make sure: Do I guess right there is no other way of doing it without using internal API?

Thanks
Bernd

Hi @BerndRuecker,

Unfortunately not :wink:

Cheers,
Roman

So, yes, your guess is right!