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 073d95332f9..9bc0da3a669 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, overrideDefinitionTenantId, predefinedProcessInstanceId, variables, transientVariables, callbackId, callbackType, - referenceId, referenceType, stageInstanceId, false); + referenceId, referenceType, 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 e09e9c7f41a..0c8147f1b3e 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; @@ -75,6 +76,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) { @@ -112,6 +115,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(); } @@ -238,7 +243,7 @@ protected boolean isFormFieldValidationEnabled(ProcessEngineConfigurationImpl pr protected ProcessInstance startProcessInstance(ProcessDefinition processDefinition) { return processInstanceHelper.createProcessInstance(processDefinition, businessKey, businessStatus, processInstanceName, overrideDefinitionTenantId, predefinedProcessInstanceId, variables, transientVariables, - callbackId, callbackType, referenceId, referenceType, stageInstanceId, true); + callbackId, callbackType, referenceId, referenceType, 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 25623b696c1..490fb6beec3 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; @@ -52,6 +54,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; @@ -92,7 +96,7 @@ public ProcessInstanceBuilder businessKey(String businessKey) { this.businessKey = businessKey; return this; } - + @Override public ProcessInstanceBuilder businessStatus(String businessStatus) { this.businessStatus = businessStatus; @@ -104,7 +108,7 @@ public ProcessInstanceBuilder callbackId(String callbackId) { this.callbackId = callbackId; return this; } - + @Override public ProcessInstanceBuilder callbackType(String callbackType) { this.callbackType = callbackType; @@ -134,7 +138,7 @@ public ProcessInstanceBuilder tenantId(String tenantId) { this.tenantId = tenantId; return this; } - + @Override public ProcessInstanceBuilder overrideProcessDefinitionTenantId(String tenantId) { this.overrideDefinitionTenantId = tenantId; @@ -232,13 +236,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); @@ -272,7 +335,7 @@ public String getProcessInstanceName() { public String getBusinessKey() { return businessKey; } - + public String getBusinessStatus() { return businessStatus; } @@ -340,4 +403,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 265f96ef1ea..fea50e10d44 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.constants.BpmnXMLConstants; @@ -76,12 +77,13 @@ public ProcessInstance createProcessInstance(ProcessDefinition processDefinition Map variables, Map transientVariables) { return createProcessInstance(processDefinition, businessKey, businessStatus, processInstanceName, null, null, - variables, transientVariables, null, null, null, null, null, false); + variables, transientVariables, null, null, null, null, null, null, null, false); } public ProcessInstance createProcessInstance(ProcessDefinition processDefinition, String businessKey, String businessStatus, String processInstanceName, String overrideDefinitionTenantId, String predefinedProcessInstanceId, Map variables, Map transientVariables, - String callbackId, String callbackType, String referenceId, String referenceType, String stageInstanceId, boolean startProcessInstance) { + String callbackId, String callbackType, String referenceId, String referenceType, String stageInstanceId, Map> userIdentityLinks, + Map> groupIdentityLinks, boolean startProcessInstance) { CommandContext commandContext = Context.getCommandContext(); if (Flowable5Util.isFlowable5ProcessDefinition(processDefinition, commandContext)) { @@ -109,7 +111,7 @@ public ProcessInstance createProcessInstance(ProcessDefinition processDefinition return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, processInstanceName, overrideDefinitionTenantId, predefinedProcessInstanceId, initialFlowElement, process, variables, transientVariables, - callbackId, callbackType, referenceId, referenceType, stageInstanceId, startProcessInstance); + callbackId, callbackType, referenceId, referenceType, stageInstanceId, userIdentityLinks, groupIdentityLinks, startProcessInstance); } public ProcessInstance createAndStartProcessInstanceByMessage(ProcessDefinition processDefinition, String messageName, String businessKey, @@ -153,7 +155,7 @@ public ProcessInstance createAndStartProcessInstanceByMessage(ProcessDefinition } return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, null, null, null, initialFlowElement, - process, variables, transientVariables, callbackId, callbackType, referenceId, referenceType, null, true); + process, variables, transientVariables, callbackId, callbackType, referenceId, referenceType, null, null, null, true); } public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(ProcessDefinition processDefinition, @@ -162,7 +164,7 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce Map transientVariables, boolean startProcessInstance) { return createAndStartProcessInstanceWithInitialFlowElement(processDefinition, businessKey, businessStatus, processInstanceName, null, null, - initialFlowElement, process, variables, transientVariables, null, null, null, null, null, startProcessInstance); + initialFlowElement, process, variables, transientVariables, null, null, null, null, null, null, null, startProcessInstance); } public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(ProcessDefinition processDefinition, @@ -171,7 +173,8 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce FlowElement initialFlowElement, Process process, Map variables, Map transientVariables, String callbackId, String callbackType, String referenceId, String referenceType, - String stageInstanceId, boolean startProcessInstance) { + String stageInstanceId, Map> userIdentityLinks, + Map> groupIdentityLinks, boolean startProcessInstance) { CommandContext commandContext = Context.getCommandContext(); @@ -191,7 +194,8 @@ public ProcessInstance createAndStartProcessInstanceWithInitialFlowElement(Proce StartProcessInstanceBeforeContext startInstanceBeforeContext = new StartProcessInstanceBeforeContext(businessKey, businessStatus, processInstanceName, callbackId, callbackType, referenceId, referenceType, variables, transientVariables, tenantId, initiatorVariableName, initialFlowElement.getId(), - initialFlowElement, process, processDefinition, overrideDefinitionTenantId, predefinedProcessInstanceId); + initialFlowElement, process, processDefinition, overrideDefinitionTenantId, predefinedProcessInstanceId, + userIdentityLinks, groupIdentityLinks); ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); if (processEngineConfiguration.getStartProcessInstanceInterceptor() != null) { @@ -240,7 +244,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 a95cf2cf55c..37c3bdbbc41 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; @@ -28,7 +29,9 @@ public class StartProcessInstanceBeforeContext extends AbstractStartProcessInsta protected String initiatorVariableName; protected String overrideDefinitionTenantId; protected String predefinedProcessInstanceId; - + protected Map> userIdentityLinks; + protected Map> groupIdentityLinks; + public StartProcessInstanceBeforeContext() { } @@ -37,7 +40,8 @@ public StartProcessInstanceBeforeContext(String businessKey, String businessStat String callbackId, String callbackType, String referenceId, String referenceType, Map variables, Map transientVariables, String tenantId, 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); @@ -50,6 +54,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() { @@ -115,4 +121,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 4fb1693cf2a..bea02d5dbf9 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. @@ -160,6 +162,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; + } +}