diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceAsyncCmd.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceAsyncCmd.java index 5f28ff1a4f0..967741a85b3 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceAsyncCmd.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceAsyncCmd.java @@ -44,7 +44,7 @@ public ProcessInstance execute(CommandContext commandContext) { processInstanceHelper = processEngineConfiguration.getProcessInstanceHelper(); ExecutionEntity processInstance = (ExecutionEntity) processInstanceHelper.createProcessInstance(processDefinition, businessKey, businessStatus, processInstanceName, startEventId, overrideDefinitionTenantId, predefinedProcessInstanceId, variables, transientVariables, callbackId, callbackType, - referenceId, referenceType, ownerId, assigneeId, stageInstanceId, false); + referenceId, referenceType, ownerId, assigneeId, stageInstanceId, userIdentityLinks, groupIdentityLinks, false); ExecutionEntity execution = processInstance.getExecutions().get(0); Process process = ProcessDefinitionUtil.getProcess(processInstance.getProcessDefinitionId()); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceCmd.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceCmd.java index a6d67ae76e7..030094970ac 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceCmd.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/StartProcessInstanceCmd.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BpmnModel; @@ -78,6 +79,8 @@ public class StartProcessInstanceCmd implements Command, Ser protected FormInfo extraFormInfo; protected String extraFormOutcome; protected boolean fallbackToDefaultTenant; + protected Map> userIdentityLinks; + protected Map> groupIdentityLinks; protected ProcessInstanceHelper processInstanceHelper; public StartProcessInstanceCmd(String processDefinitionKey, String processDefinitionId, String businessKey, Map variables) { @@ -118,6 +121,8 @@ public StartProcessInstanceCmd(ProcessInstanceBuilderImpl processInstanceBuilder this.extraFormInfo = processInstanceBuilder.getExtraFormInfo(); this.extraFormOutcome = processInstanceBuilder.getExtraFormOutcome(); this.fallbackToDefaultTenant = processInstanceBuilder.isFallbackToDefaultTenant(); + this.userIdentityLinks = processInstanceBuilder.getUserIdentityLinks(); + this.groupIdentityLinks = processInstanceBuilder.getGroupIdentityLinks(); this.businessStatus = processInstanceBuilder.getBusinessStatus(); } @@ -266,7 +271,7 @@ protected boolean isFormFieldValidationEnabled(ProcessEngineConfigurationImpl pr protected ProcessInstance startProcessInstance(ProcessDefinition processDefinition) { return processInstanceHelper.createProcessInstance(processDefinition, businessKey, businessStatus, processInstanceName, startEventId, overrideDefinitionTenantId, predefinedProcessInstanceId, variables, transientVariables, - callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, stageInstanceId, true); + callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, stageInstanceId, userIdentityLinks, groupIdentityLinks, true); } protected boolean hasStartFormData() { diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/runtime/ProcessInstanceBuilderImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/runtime/ProcessInstanceBuilderImpl.java index 4e3ddf38b77..932aeee77b8 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/runtime/ProcessInstanceBuilderImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/runtime/ProcessInstanceBuilderImpl.java @@ -13,7 +13,9 @@ package org.flowable.engine.impl.runtime; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.engine.impl.RuntimeServiceImpl; @@ -55,6 +57,8 @@ public class ProcessInstanceBuilderImpl implements ProcessInstanceBuilder { protected FormInfo extraFormInfo; protected String extraFormOutcome; protected boolean fallbackToDefaultTenant; + protected Map> userIdentityLinks; + protected Map> groupIdentityLinks; public ProcessInstanceBuilderImpl(RuntimeServiceImpl runtimeService) { this.runtimeService = runtimeService; @@ -101,7 +105,7 @@ public ProcessInstanceBuilder businessKey(String businessKey) { this.businessKey = businessKey; return this; } - + @Override public ProcessInstanceBuilder businessStatus(String businessStatus) { this.businessStatus = businessStatus; @@ -113,7 +117,7 @@ public ProcessInstanceBuilder callbackId(String callbackId) { this.callbackId = callbackId; return this; } - + @Override public ProcessInstanceBuilder callbackType(String callbackType) { this.callbackType = callbackType; @@ -143,7 +147,7 @@ public ProcessInstanceBuilder tenantId(String tenantId) { this.tenantId = tenantId; return this; } - + @Override public ProcessInstanceBuilder overrideProcessDefinitionTenantId(String tenantId) { this.overrideDefinitionTenantId = tenantId; @@ -253,13 +257,72 @@ public ProcessInstanceBuilder formVariables(Map formVariables, F return this; } - @Override public ProcessInstanceBuilder fallbackToDefaultTenant() { this.fallbackToDefaultTenant = true; return this; } + @Override + public ProcessInstanceBuilder userIdentityLinks(Map> userIdentityLinks) { + if (userIdentityLinks != null) { + if (this.userIdentityLinks == null) { + this.userIdentityLinks = new HashMap<>(); + } + mergeIdentityLinks(this.userIdentityLinks, userIdentityLinks); + } + return this; + } + + @Override + public ProcessInstanceBuilder userIdentityLink(String identityLinkType, String user) { + if (identityLinkType != null && user != null) { + Set users = new HashSet<>(); + users.add(user); + Map> identityLinkMap = new HashMap<>(); + identityLinkMap.put(identityLinkType, users); + userIdentityLinks(identityLinkMap); + } + return this; + } + + @Override + public ProcessInstanceBuilder groupIdentityLinks(Map> groupIdentityLinks) { + if (groupIdentityLinks != null) { + if (this.groupIdentityLinks == null) { + this.groupIdentityLinks = new HashMap<>(); + } + mergeIdentityLinks(this.groupIdentityLinks, groupIdentityLinks); + } + return this; + } + + @Override + public ProcessInstanceBuilder groupIdentityLink(String identityLinkType, String group) { + if (identityLinkType != null && group != null) { + Set groups = new HashSet<>(); + groups.add(group); + Map> identityLinkMap = new HashMap<>(); + identityLinkMap.put(identityLinkType, groups); + groupIdentityLinks(identityLinkMap); + } + return this; + } + + private void mergeIdentityLinks(Map> target, Map> additionalLinks) { + for (Map.Entry> additionalLink : additionalLinks.entrySet()) { + String linkType = additionalLink.getKey(); + Set parties = additionalLink.getValue(); + if (linkType != null && parties != null) { + target.merge(linkType, parties, (party1, party2) -> { + Set combinedParties = new HashSet(party1); + combinedParties.addAll(party2); + return combinedParties; + }); + } + } + } + @Override public ProcessInstance start() { return runtimeService.startProcessInstance(this); @@ -297,7 +360,7 @@ public String getProcessInstanceName() { public String getBusinessKey() { return businessKey; } - + public String getBusinessStatus() { return businessStatus; } @@ -373,4 +436,12 @@ public boolean isFallbackToDefaultTenant() { return fallbackToDefaultTenant; } + public Map> getUserIdentityLinks() { + return userIdentityLinks; + } + + public Map> getGroupIdentityLinks() { + return groupIdentityLinks; + } + } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java index 5320b069d82..be69a94e546 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.EventSubProcess; @@ -68,13 +69,13 @@ public ProcessInstance createProcessInstance(ProcessDefinition processDefinition Map variables, Map transientVariables, String ownerId, String assigneeId) { return createProcessInstance(processDefinition, businessKey, businessStatus, processInstanceName, null, null, null, - variables, transientVariables, null, null, null, null, ownerId, assigneeId, null, false); + variables, transientVariables, null, null, null, null, ownerId, assigneeId, null, null, null, false); } public ProcessInstance createProcessInstance(ProcessDefinition processDefinition, String businessKey, String businessStatus, String processInstanceName, String startEventId, String overrideDefinitionTenantId, String predefinedProcessInstanceId, Map variables, Map transientVariables, String callbackId, String callbackType, String referenceId, String referenceType, String ownerId, String assigneeId, - String stageInstanceId, boolean startProcessInstance) { + String stageInstanceId, Map> userIdentityLinks, Map> groupIdentityLinks, boolean startProcessInstance) { CommandContext commandContext = Context.getCommandContext(); if (Flowable5Util.isFlowable5ProcessDefinition(processDefinition, commandContext)) { @@ -118,7 +119,7 @@ public ProcessInstance createProcessInstance(ProcessDefinition processDefinition return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, processInstanceName, overrideDefinitionTenantId, predefinedProcessInstanceId, initialFlowElement, process, variables, transientVariables, - callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, stageInstanceId, startProcessInstance); + callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, stageInstanceId, userIdentityLinks, groupIdentityLinks, startProcessInstance); } public ProcessInstance createAndStartProcessInstanceByMessage(ProcessDefinition processDefinition, String messageName, String businessKey, @@ -160,7 +161,7 @@ public ProcessInstance createAndStartProcessInstanceByMessage(ProcessDefinition } return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, null, null, null, initialFlowElement, - process, variables, transientVariables, callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, null, true); + process, variables, transientVariables, callbackId, callbackType, referenceId, referenceType, ownerId, assigneeId, null, null, null, true); } public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(ProcessDefinition processDefinition, @@ -170,7 +171,7 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, processInstanceName, null, null, initialFlowElement, process, variables, transientVariables, null, null, null, null, - ownerId, assigneeId, null, startProcessInstance); + ownerId, assigneeId, null, null, null, startProcessInstance); } public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(ProcessDefinition processDefinition, @@ -179,7 +180,8 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce FlowElement initialFlowElement, Process process, Map variables, Map transientVariables, String callbackId, String callbackType, String referenceId, String referenceType, String ownerId, String assigneeId, - String stageInstanceId, boolean startProcessInstance) { + String stageInstanceId, Map> userIdentityLinks, + Map> groupIdentityLinks, boolean startProcessInstance) { CommandContext commandContext = Context.getCommandContext(); @@ -199,7 +201,8 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce StartProcessInstanceBeforeContext startInstanceBeforeContext = new StartProcessInstanceBeforeContext(businessKey, businessStatus, processInstanceName, callbackId, callbackType, referenceId, referenceType, variables, transientVariables, tenantId, ownerId, assigneeId, initiatorVariableName, initialFlowElement.getId(), - initialFlowElement, process, processDefinition, overrideDefinitionTenantId, predefinedProcessInstanceId); + initialFlowElement, process, processDefinition, overrideDefinitionTenantId, predefinedProcessInstanceId, + userIdentityLinks, groupIdentityLinks); ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); if (processEngineConfiguration.getStartProcessInstanceInterceptor() != null) { @@ -256,7 +259,35 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce processInstance.setTransientVariable(varName, startInstanceBeforeContext.getTransientVariables().get(varName)); } } - + + if (startInstanceBeforeContext.getUserIdentityLinks() != null) { + for (Map.Entry> entry : startInstanceBeforeContext.getUserIdentityLinks().entrySet()) { + String identityLinkType = entry.getKey(); + Set users = entry.getValue(); + if (identityLinkType != null && users != null) { + for (String user : users) { + if (user != null) { + IdentityLinkUtil.createProcessInstanceIdentityLink(processInstance, user, null, identityLinkType); + } + } + } + } + } + + if (startInstanceBeforeContext.getGroupIdentityLinks() != null) { + for (Map.Entry> entry : startInstanceBeforeContext.getGroupIdentityLinks().entrySet()) { + String identityLinkType = entry.getKey(); + Set groups = entry.getValue(); + if (identityLinkType != null && groups != null) { + for (String group : groups) { + if (group != null) { + IdentityLinkUtil.createProcessInstanceIdentityLink(processInstance, null, group, identityLinkType); + } + } + } + } + } + // Fire events if (eventDispatcherEnabled) { eventDispatcher.dispatchEvent(FlowableEventBuilder.createEntityWithVariablesEvent(FlowableEngineEventType.ENTITY_INITIALIZED, diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/interceptor/StartProcessInstanceBeforeContext.java b/modules/flowable-engine/src/main/java/org/flowable/engine/interceptor/StartProcessInstanceBeforeContext.java index f9e2d3fbc30..648e5df5d5f 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/interceptor/StartProcessInstanceBeforeContext.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/interceptor/StartProcessInstanceBeforeContext.java @@ -13,6 +13,7 @@ package org.flowable.engine.interceptor; import java.util.Map; +import java.util.Set; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.Process; @@ -30,7 +31,9 @@ public class StartProcessInstanceBeforeContext extends AbstractStartProcessInsta protected String initiatorVariableName; protected String overrideDefinitionTenantId; protected String predefinedProcessInstanceId; - + protected Map> userIdentityLinks; + protected Map> groupIdentityLinks; + public StartProcessInstanceBeforeContext() { } @@ -39,7 +42,8 @@ public StartProcessInstanceBeforeContext(String businessKey, String businessStat String callbackId, String callbackType, String referenceId, String referenceType, Map variables, Map transientVariables, String tenantId, String ownerId, String assigneeId, String initiatorVariableName, String initialActivityId, FlowElement initialFlowElement, Process process, - ProcessDefinition processDefinition, String overrideDefinitionTenantId, String predefinedProcessInstanceId) { + ProcessDefinition processDefinition, String overrideDefinitionTenantId, String predefinedProcessInstanceId, + Map> userIdentityLinks, Map> groupIdentityLinks) { super(businessKey, businessStatus, processInstanceName, variables, transientVariables, initialActivityId, initialFlowElement, process, processDefinition); @@ -54,6 +58,8 @@ public StartProcessInstanceBeforeContext(String businessKey, String businessStat this.initiatorVariableName = initiatorVariableName; this.overrideDefinitionTenantId = overrideDefinitionTenantId; this.predefinedProcessInstanceId = predefinedProcessInstanceId; + this.userIdentityLinks = userIdentityLinks; + this.groupIdentityLinks = groupIdentityLinks; } public String getCallbackId() { @@ -135,4 +141,20 @@ public String getPredefinedProcessInstanceId() { public void setPredefinedProcessInstanceId(String predefinedProcessInstanceId) { this.predefinedProcessInstanceId = predefinedProcessInstanceId; } + + public Map> getUserIdentityLinks() { + return userIdentityLinks; + } + + public void setUserIdentityLinks(Map> userIdentityLinks) { + this.userIdentityLinks = userIdentityLinks; + } + + public Map> getGroupIdentityLinks() { + return groupIdentityLinks; + } + + public void setGroupIdentityLinks(Map> groupIdentityLinks) { + this.groupIdentityLinks = groupIdentityLinks; + } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/runtime/ProcessInstanceBuilder.java b/modules/flowable-engine/src/main/java/org/flowable/engine/runtime/ProcessInstanceBuilder.java index 5620e7496ff..aab7aeaf341 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/runtime/ProcessInstanceBuilder.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/runtime/ProcessInstanceBuilder.java @@ -13,10 +13,12 @@ package org.flowable.engine.runtime; import java.util.Map; +import java.util.Set; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.form.api.FormInfo; +import org.flowable.identitylink.api.IdentityLinkType; /** * Helper for starting new ProcessInstance. @@ -179,6 +181,32 @@ public interface ProcessInstanceBuilder { */ ProcessInstanceBuilder fallbackToDefaultTenant(); + /** + * Adds user identity links to the process instance. + * + * @param userIdentityLinks + * for each identity link type (@see {@link IdentityLinkType}), a set of users can be provided; null values will be ignored + */ + ProcessInstanceBuilder userIdentityLinks(Map> userIdentityLinks); + + /** + * Adds a user identity link to the process instance + */ + ProcessInstanceBuilder userIdentityLink(String identityLinkType, String user); + + /** + * Adds group identity links to the process instance. + * + * @param groupIdentityLinks + * for each identity link type (@see {@link IdentityLinkType}), a set of groups can be provided; null values will be ignored + */ + ProcessInstanceBuilder groupIdentityLinks(Map> groupIdentityLinks); + + /** + * Adds a group identity link to the process instance + */ + ProcessInstanceBuilder groupIdentityLink(String identityLinkType, String group); + /** * Start the process instance * diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/ProcessInstanceCreateWithIdentityLinkTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/ProcessInstanceCreateWithIdentityLinkTest.java new file mode 100644 index 00000000000..e702bfbf12b --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/ProcessInstanceCreateWithIdentityLinkTest.java @@ -0,0 +1,186 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.flowable.engine.test.api.runtime; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.flowable.engine.impl.test.PluggableFlowableTestCase; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.test.Deployment; +import org.flowable.identitylink.api.IdentityLink; +import org.flowable.identitylink.api.IdentityLinkInfo; +import org.flowable.identitylink.api.IdentityLinkType; +import org.junit.jupiter.api.Test; + +class ProcessInstanceCreateWithIdentityLinkTest extends PluggableFlowableTestCase { + + static final String USER_ALICE = "Alice"; + static final String USER_BOB = "Bob"; + + static final String GROUP_ALPHA = "alpha"; + static final String GROUP_BETA = "beta"; + static final String GROUP_GAMMA = "gamma"; + static final String GROUP_DELTA = "delta"; + + static final String CUSTOM_LINK_TYPE_1 = "custom_test_type_1"; + static final String CUSTOM_LINK_TYPE_2 = "custom_test_type_2"; + static final String CUSTOM_LINK_TYPE_3 = "custom_test_type_3"; + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + void testProcessInstanceCreateWithSingleUserLink() { + + ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess") + .userIdentityLink(IdentityLinkType.OWNER, USER_ALICE).start(); + + List identityLinks = runtimeService.getIdentityLinksForProcessInstance(processInstance.getId()); + assertUserLinks(IdentityLinkType.OWNER, identityLinks, USER_ALICE); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + void testProcessInstanceCreateWithSeveralUserLinksAddedOneByOne() { + + ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess") + .userIdentityLink(IdentityLinkType.OWNER, USER_BOB) + .userIdentityLink(CUSTOM_LINK_TYPE_1, USER_ALICE) + .userIdentityLink(CUSTOM_LINK_TYPE_1, USER_BOB) + .start(); + + List identityLinks = runtimeService.getIdentityLinksForProcessInstance(processInstance.getId()); + assertThat(identityLinks).hasSize(3); + + assertUserLinks(IdentityLinkType.OWNER, identityLinks, USER_BOB); + assertUserLinks(CUSTOM_LINK_TYPE_1, identityLinks, USER_ALICE, USER_BOB); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + void testProcessInstanceCreateWithSeveralUserLinksInBulk() { + + Map> userIdentityLinks = new HashMap<>(); + userIdentityLinks.put(IdentityLinkType.OWNER, createSet(USER_ALICE)); + userIdentityLinks.put(CUSTOM_LINK_TYPE_1, createSet(USER_ALICE, USER_BOB)); + + ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess") + .userIdentityLinks(userIdentityLinks) + .start(); + + List identityLinks = runtimeService.getIdentityLinksForProcessInstance(processInstance.getId()); + assertThat(identityLinks).hasSize(3); + + assertUserLinks(IdentityLinkType.OWNER, identityLinks, USER_ALICE); + assertUserLinks(CUSTOM_LINK_TYPE_1, identityLinks, USER_ALICE, USER_BOB); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + void testProcessInstanceCreateWithSeveralUsersAndGroups() { + + Map> userIdentityLinks = new HashMap<>(); + userIdentityLinks.put(IdentityLinkType.OWNER, createSet(USER_ALICE)); + userIdentityLinks.put(CUSTOM_LINK_TYPE_1, createSet(USER_ALICE, USER_BOB)); + + Map> groupIdentityLinks = new HashMap<>(); + groupIdentityLinks.put(CUSTOM_LINK_TYPE_1, createSet(GROUP_ALPHA, GROUP_BETA)); + groupIdentityLinks.put(CUSTOM_LINK_TYPE_2, createSet(GROUP_GAMMA)); + + ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess") + .userIdentityLinks(userIdentityLinks) + .groupIdentityLink(CUSTOM_LINK_TYPE_1, GROUP_ALPHA) + .groupIdentityLinks(groupIdentityLinks) + .groupIdentityLink(CUSTOM_LINK_TYPE_2, GROUP_DELTA) + .start(); + + List identityLinks = runtimeService.getIdentityLinksForProcessInstance(processInstance.getId()); + assertThat(identityLinks).hasSize(7); + + assertUserLinks(IdentityLinkType.OWNER, identityLinks, USER_ALICE); + assertUserLinks(CUSTOM_LINK_TYPE_1, identityLinks, USER_ALICE, USER_BOB); + + assertGroupLinks(CUSTOM_LINK_TYPE_1, identityLinks, GROUP_ALPHA, GROUP_BETA); + assertGroupLinks(CUSTOM_LINK_TYPE_2, identityLinks, GROUP_GAMMA, GROUP_DELTA); + } + + @Test + @Deployment(resources = "org/flowable/engine/test/api/runtime/oneTaskProcess.bpmn20.xml") + void testProcessInstanceCreateWithIdentityLinksToBeIgnored() { + + Set setContainingNull = new HashSet(); + setContainingNull.add(null); + + Map> userLinksContainingNullValues = new HashMap<>(); + userLinksContainingNullValues.put(null, createSet(USER_ALICE)); + userLinksContainingNullValues.put(CUSTOM_LINK_TYPE_2, null); + userLinksContainingNullValues.put(CUSTOM_LINK_TYPE_3, setContainingNull); + + Map> groupLinksContainingNullValues = new HashMap<>(); + groupLinksContainingNullValues.put(null, createSet(GROUP_ALPHA)); + groupLinksContainingNullValues.put(CUSTOM_LINK_TYPE_2, null); + groupLinksContainingNullValues.put(CUSTOM_LINK_TYPE_3, setContainingNull); + + ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("oneTaskProcess") + .userIdentityLink(CUSTOM_LINK_TYPE_1, USER_ALICE) + .userIdentityLink(null, null) + .userIdentityLink(CUSTOM_LINK_TYPE_2, null) + .userIdentityLink(null, USER_BOB) + .userIdentityLinks(userLinksContainingNullValues) + .groupIdentityLink(CUSTOM_LINK_TYPE_1, GROUP_ALPHA) + .groupIdentityLink(null, null) + .groupIdentityLink(CUSTOM_LINK_TYPE_2, null) + .groupIdentityLink(null, GROUP_BETA) + .groupIdentityLinks(groupLinksContainingNullValues) + .start(); + + List identityLinks = runtimeService.getIdentityLinksForProcessInstance(processInstance.getId()); + assertThat(identityLinks).hasSize(2); + + assertUserLinks(CUSTOM_LINK_TYPE_1, identityLinks, USER_ALICE); + assertGroupLinks(CUSTOM_LINK_TYPE_1, identityLinks, GROUP_ALPHA); + } + + private void assertUserLinks(String linkType, List unfilteredLinks, String... userIds) { + assertLinks(linkType, unfilteredLinks, IdentityLinkInfo::getUserId, userIds); + } + + private void assertGroupLinks(String linkType, List unfilteredLinks, String... groupIds) { + assertLinks(linkType, unfilteredLinks, IdentityLinkInfo::getGroupId, groupIds); + } + + private void assertLinks(String linkType, List unfilteredLinks, + Function idRetriever, String... expectedIds) { + + Stream relevantIds = unfilteredLinks.stream() + .filter(identityLink -> identityLink.getType().equals(linkType)) + .map(idRetriever) + .filter(Objects::nonNull); + assertThat(relevantIds).containsOnly(expectedIds); + } + + private Set createSet(String... entries) { + Set stringSet = new HashSet<>(); + for (String entry : entries) { + stringSet.add(entry); + } + return stringSet; + } +}