Skip to content

Commit b5b39c7

Browse files
committed
Customizable case-start lifecycle on the CMMN side
Mirror the BPMN ProcessLevelStartEventActivityBehavior pattern on the CMMN side: a CaseDefinitionStartLifecycleHandler attached to the Case at parse time owns its deploy/undeploy. The deploy context has an isRestoringPreviousVersion() flag for parity, and an EventRegistryCaseDefinitionStartLifecycleHandler is installed by default when case.startEventType is set. A new findEventSubscriptionsByTypesAndScopeDefinitionId finder lets CmmnDeployer issue a single bulk-delete after the undeploy iteration.
1 parent af24d6c commit b5b39c7

19 files changed

Lines changed: 652 additions & 139 deletions

File tree

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
import org.flowable.cmmn.engine.impl.parser.DefaultCmmnActivityBehaviorFactory;
126126
import org.flowable.cmmn.engine.impl.parser.handler.CasePageTaskParseHandler;
127127
import org.flowable.cmmn.engine.impl.parser.handler.CaseParseHandler;
128+
import org.flowable.cmmn.engine.impl.parser.handler.EventRegistryCaseStartLifecycleParseHandler;
128129
import org.flowable.cmmn.engine.impl.parser.handler.CaseTaskParseHandler;
129130
import org.flowable.cmmn.engine.impl.parser.handler.DecisionTaskParseHandler;
130131
import org.flowable.cmmn.engine.impl.parser.handler.ExternalWorkerServiceTaskParseHandler;
@@ -1201,6 +1202,7 @@ public void initCmmnParser() {
12011202
public List<CmmnParseHandler> getDefaultCmmnParseHandlers() {
12021203
List<CmmnParseHandler> cmmnParseHandlers = new ArrayList<>();
12031204
cmmnParseHandlers.add(new CaseParseHandler());
1205+
cmmnParseHandlers.add(new EventRegistryCaseStartLifecycleParseHandler());
12041206
cmmnParseHandlers.add(new CaseTaskParseHandler());
12051207
cmmnParseHandlers.add(new DecisionTaskParseHandler());
12061208
cmmnParseHandlers.add(new HumanTaskParseHandler());
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
package org.flowable.cmmn.engine.impl.deployer;
14+
15+
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
16+
import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity;
17+
import org.flowable.cmmn.model.Case;
18+
import org.flowable.common.engine.impl.interceptor.CommandContext;
19+
import org.flowable.eventsubscription.service.EventSubscriptionService;
20+
21+
/**
22+
* Context passed to {@link CaseDefinitionStartLifecycleHandler#deploy} carrying the freshly-deployed
23+
* case definition together with its parsed {@link Case} model.
24+
*/
25+
public class CaseDefinitionStartDeployContext {
26+
27+
protected final CaseDefinitionEntity caseDefinition;
28+
protected final Case caseModel;
29+
protected final CmmnEngineConfiguration cmmnEngineConfiguration;
30+
protected final CommandContext commandContext;
31+
protected final boolean restoringPreviousVersion;
32+
33+
public CaseDefinitionStartDeployContext(CaseDefinitionEntity caseDefinition, Case caseModel,
34+
CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext) {
35+
this(caseDefinition, caseModel, cmmnEngineConfiguration, commandContext, false);
36+
}
37+
38+
public CaseDefinitionStartDeployContext(CaseDefinitionEntity caseDefinition, Case caseModel,
39+
CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext,
40+
boolean restoringPreviousVersion) {
41+
this.caseDefinition = caseDefinition;
42+
this.caseModel = caseModel;
43+
this.cmmnEngineConfiguration = cmmnEngineConfiguration;
44+
this.commandContext = commandContext;
45+
this.restoringPreviousVersion = restoringPreviousVersion;
46+
}
47+
48+
public CaseDefinitionEntity getCaseDefinition() {
49+
return caseDefinition;
50+
}
51+
52+
public Case getCaseModel() {
53+
return caseModel;
54+
}
55+
56+
public CmmnEngineConfiguration getCmmnEngineConfiguration() {
57+
return cmmnEngineConfiguration;
58+
}
59+
60+
public CommandContext getCommandContext() {
61+
return commandContext;
62+
}
63+
64+
public EventSubscriptionService getEventSubscriptionService() {
65+
return cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService();
66+
}
67+
68+
/**
69+
* {@code true} when this deploy is restoring a previous (earlier-version) case definition's start
70+
* triggers because the latest version's deployment was just deleted. Behaviors should skip duplicate-
71+
* subscription validation in this mode — the just-deleted case definition's subscriptions may still
72+
* be in the in-session entity cache (the bulk delete hasn't been flushed yet) so a re-insert would
73+
* otherwise trip a false-positive conflict. The fresh-deployment path leaves this {@code false}.
74+
*/
75+
public boolean isRestoringPreviousVersion() {
76+
return restoringPreviousVersion;
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
package org.flowable.cmmn.engine.impl.deployer;
14+
15+
/**
16+
* Implemented by an opt-in handler attached to a {@code Case} that owns a deploy-time start trigger
17+
* (event-registry subscription, timer cron, custom subscription type, etc.). The
18+
* {@link CmmnDeployer} iterates the handlers attached to each case via
19+
* {@code Case.getStartLifecycleHandlers()} and calls {@link #deploy} on the freshly-deployed case
20+
* definition's handlers / {@link #undeploy} on the previous (now-superseded) case definition's
21+
* handlers.
22+
* <p>
23+
* Restoration after deployment-deletion goes through {@link #deploy} too — the deploy context's
24+
* {@code isRestoringPreviousVersion()} flag distinguishes it from a fresh deploy. Both methods must
25+
* be implemented; a handler that opts into the deploy-time lifecycle owns both halves.
26+
* <p>
27+
* Custom integrations install additional handlers via a custom {@code CmmnParseHandler} (registered
28+
* in {@code customCmmnParseHandlers}) that calls {@code case.addStartLifecycleHandler(...)} during
29+
* parsing. Multiple handlers may co-exist on a single case.
30+
*/
31+
public interface CaseDefinitionStartLifecycleHandler {
32+
33+
/**
34+
* Register the deploy-time artifact (event subscription, timer job, etc.) for this case
35+
* definition's start trigger when it is freshly deployed. Also called via the
36+
* deployment-deletion restoration path when the previous version's start triggers are
37+
* restored — distinguished by {@link CaseDefinitionStartDeployContext#isRestoringPreviousVersion()}.
38+
*/
39+
void deploy(CaseDefinitionStartDeployContext context);
40+
41+
/**
42+
* Remove or update the deploy-time artifact for this case definition's start trigger when
43+
* its case definition is superseded by a new version. Called on the previous (now-superseded)
44+
* case definition's handlers.
45+
*/
46+
void undeploy(CaseDefinitionStartUndeployContext context);
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
package org.flowable.cmmn.engine.impl.deployer;
14+
15+
import java.util.Set;
16+
17+
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
18+
import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity;
19+
import org.flowable.cmmn.model.Case;
20+
import org.flowable.common.engine.impl.interceptor.CommandContext;
21+
import org.flowable.eventsubscription.service.EventSubscriptionService;
22+
23+
/**
24+
* Context passed to {@link CaseDefinitionStartLifecycleHandler#undeploy} when a case definition is
25+
* being superseded by a new version. Carries both the previous (now-superseded) case definition and
26+
* the new one — most handlers only need the previous case definition, but the EventRegistry "manual"
27+
* re-point branch updates subscriptions to point at {@link #getNewCaseDefinition()}.
28+
* <p>
29+
* Handlers register their obsolete event subscription types via
30+
* {@link #registerObsoleteEventSubscriptionType(String)}. The deployer issues one mass-delete per
31+
* unique registered type after the undeploy iteration — fewer DB round-trips than per-handler
32+
* deletes, and tighter than a fixed sweep that always ran regardless of which types the previous
33+
* case definition actually used.
34+
*/
35+
public class CaseDefinitionStartUndeployContext {
36+
37+
protected final CaseDefinitionEntity previousCaseDefinition;
38+
protected final CaseDefinitionEntity newCaseDefinition;
39+
protected final Case previousCaseModel;
40+
protected final CmmnEngineConfiguration cmmnEngineConfiguration;
41+
protected final CommandContext commandContext;
42+
protected final Set<String> obsoleteEventSubscriptionTypes;
43+
44+
public CaseDefinitionStartUndeployContext(CaseDefinitionEntity previousCaseDefinition, CaseDefinitionEntity newCaseDefinition,
45+
Case previousCaseModel, CmmnEngineConfiguration cmmnEngineConfiguration, CommandContext commandContext,
46+
Set<String> obsoleteEventSubscriptionTypes) {
47+
this.previousCaseDefinition = previousCaseDefinition;
48+
this.newCaseDefinition = newCaseDefinition;
49+
this.previousCaseModel = previousCaseModel;
50+
this.cmmnEngineConfiguration = cmmnEngineConfiguration;
51+
this.commandContext = commandContext;
52+
this.obsoleteEventSubscriptionTypes = obsoleteEventSubscriptionTypes;
53+
}
54+
55+
public CaseDefinitionEntity getPreviousCaseDefinition() {
56+
return previousCaseDefinition;
57+
}
58+
59+
public CaseDefinitionEntity getNewCaseDefinition() {
60+
return newCaseDefinition;
61+
}
62+
63+
public Case getPreviousCaseModel() {
64+
return previousCaseModel;
65+
}
66+
67+
public CmmnEngineConfiguration getCmmnEngineConfiguration() {
68+
return cmmnEngineConfiguration;
69+
}
70+
71+
public CommandContext getCommandContext() {
72+
return commandContext;
73+
}
74+
75+
public EventSubscriptionService getEventSubscriptionService() {
76+
return cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService();
77+
}
78+
79+
/**
80+
* Registers an event subscription event-type that the deployer should mass-delete for the previous
81+
* case definition (scope-type CMMN, scope-id null) after the undeploy iteration. Multiple handlers
82+
* registering the same type result in a single DB sweep.
83+
*/
84+
public void registerObsoleteEventSubscriptionType(String eventType) {
85+
obsoleteEventSubscriptionTypes.add(eventType);
86+
}
87+
}

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@
1313
package org.flowable.cmmn.engine.impl.deployer;
1414

1515
import java.util.Collection;
16-
import java.util.Collections;
1716
import java.util.LinkedHashMap;
1817
import java.util.LinkedHashSet;
1918
import java.util.List;
2019
import java.util.Map;
21-
import java.util.Objects;
2220
import java.util.Set;
2321

2422
import org.apache.commons.lang3.StringUtils;
25-
import org.flowable.cmmn.converter.CmmnXmlConstants;
2623
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
2724
import org.flowable.cmmn.engine.impl.parser.CmmnParseContext;
2825
import org.flowable.cmmn.engine.impl.parser.CmmnParseResult;
@@ -32,11 +29,9 @@
3229
import org.flowable.cmmn.engine.impl.persistence.entity.CmmnDeploymentEntity;
3330
import org.flowable.cmmn.engine.impl.persistence.entity.CmmnResourceEntity;
3431
import org.flowable.cmmn.engine.impl.persistence.entity.deploy.CaseDefinitionCacheEntry;
35-
import org.flowable.cmmn.engine.impl.util.CmmnCorrelationUtil;
3632
import org.flowable.cmmn.engine.impl.util.CommandContextUtil;
3733
import org.flowable.cmmn.model.Case;
3834
import org.flowable.cmmn.model.CmmnModel;
39-
import org.flowable.cmmn.model.ExtensionElement;
4035
import org.flowable.cmmn.validation.CaseValidator;
4136
import org.flowable.common.engine.api.FlowableException;
4237
import org.flowable.common.engine.api.delegate.Expression;
@@ -46,9 +41,12 @@
4641
import org.flowable.common.engine.impl.EngineDeployer;
4742
import org.flowable.common.engine.impl.assignment.CandidateUtil;
4843
import org.flowable.common.engine.impl.cfg.IdGenerator;
44+
import org.flowable.common.engine.impl.context.Context;
4945
import org.flowable.common.engine.impl.el.ExpressionManager;
46+
import org.flowable.common.engine.impl.interceptor.CommandContext;
5047
import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache;
5148
import org.flowable.eventsubscription.service.EventSubscriptionService;
49+
import org.flowable.eventsubscription.service.impl.persistence.entity.EventSubscriptionEntity;
5250
import org.flowable.identitylink.api.IdentityLinkType;
5351
import org.flowable.identitylink.service.IdentityLinkService;
5452
import org.flowable.identitylink.service.impl.persistence.entity.IdentityLinkEntity;
@@ -193,39 +191,45 @@ protected void persistCaseDefinitions(CmmnParseResult parseResult) {
193191

194192
protected void updateEventSubscriptions(CmmnParseResult parseResult, Map<CaseDefinitionEntity, CaseDefinitionEntity> mapOfNewCaseDefinitionToPreviousVersion) {
195193
EventSubscriptionService eventSubscriptionService = cmmnEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService();
196-
for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) {
194+
CommandContext commandContext = Context.getCommandContext();
197195

196+
for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) {
197+
Case newCaseModel = parseResult.getCmmnCaseForCaseDefinition(caseDefinition);
198198
CaseDefinitionEntity previousCaseDefinition = mapOfNewCaseDefinitionToPreviousVersion.get(caseDefinition);
199+
199200
if (previousCaseDefinition != null) {
200-
if (isManualCorrelationSubscriptionConfiguration(parseResult, previousCaseDefinition)) {
201-
// for a dynamic event registry start event, we don't remove the manually registered subscriptions, but rather update them to the newest
202-
// case definition, if required
203-
String startEventType = getCaseModel(parseResult, previousCaseDefinition).getPrimaryCase().getStartEventType();
204-
updateOldEventSubscriptions(previousCaseDefinition, caseDefinition, startEventType);
205-
} else {
206-
// for a static event registry start event, we delete the old subscription and will later create a new one
207-
eventSubscriptionService.deleteEventSubscriptionsForScopeDefinitionIdAndTypeAndNullScopeId(previousCaseDefinition.getId(), ScopeTypes.CMMN);
201+
Case previousCaseModel = getCaseModel(parseResult, previousCaseDefinition).getPrimaryCase();
202+
Set<String> obsoleteEventTypes = new LinkedHashSet<>();
203+
204+
for (Object handler : previousCaseModel.getStartLifecycleHandlers()) {
205+
if (handler instanceof CaseDefinitionStartLifecycleHandler lifecycleHandler) {
206+
lifecycleHandler.undeploy(new CaseDefinitionStartUndeployContext(
207+
previousCaseDefinition, caseDefinition, previousCaseModel,
208+
cmmnEngineConfiguration, commandContext, obsoleteEventTypes));
209+
}
210+
}
211+
212+
if (!obsoleteEventTypes.isEmpty()) {
213+
deleteObsoleteEventSubscriptions(previousCaseDefinition, obsoleteEventTypes, eventSubscriptionService);
208214
}
209215
}
210216

211-
// create new subscriptions, but only for static event registry start events
212-
Case caseModel = parseResult.getCmmnCaseForCaseDefinition(caseDefinition);
213-
String startEventType = caseModel.getStartEventType();
214-
if (startEventType != null && !isManualCorrelationSubscriptionConfiguration(parseResult, caseDefinition)) {
215-
eventSubscriptionService.createEventSubscriptionBuilder()
216-
.eventType(startEventType)
217-
.configuration(getEventCorrelationKey(caseModel))
218-
.scopeDefinitionId(caseDefinition.getId())
219-
.scopeType(ScopeTypes.CMMN)
220-
.tenantId(caseDefinition.getTenantId())
221-
.create();
217+
for (Object handler : newCaseModel.getStartLifecycleHandlers()) {
218+
if (handler instanceof CaseDefinitionStartLifecycleHandler lifecycleHandler) {
219+
lifecycleHandler.deploy(new CaseDefinitionStartDeployContext(
220+
caseDefinition, newCaseModel, cmmnEngineConfiguration, commandContext));
221+
}
222222
}
223223
}
224224
}
225225

226-
protected void updateOldEventSubscriptions(CaseDefinitionEntity previousCaseDefinition, CaseDefinitionEntity caseDefinition, String eventType) {
227-
CommandContextUtil.getCmmnEngineConfiguration().getEventSubscriptionServiceConfiguration().getEventSubscriptionService().updateEventSubscriptionScopeDefinitionId(
228-
previousCaseDefinition.getId(), caseDefinition.getId(), eventType, caseDefinition.getKey(), null);
226+
protected void deleteObsoleteEventSubscriptions(CaseDefinitionEntity previousCaseDefinition, Collection<String> eventTypes,
227+
EventSubscriptionService eventSubscriptionService) {
228+
List<EventSubscriptionEntity> subscriptionsToDelete = eventSubscriptionService.findEventSubscriptionsByTypesAndScopeDefinitionId(
229+
eventTypes, previousCaseDefinition.getId(), ScopeTypes.CMMN, previousCaseDefinition.getTenantId());
230+
for (EventSubscriptionEntity subscription : subscriptionsToDelete) {
231+
eventSubscriptionService.deleteEventSubscription(subscription);
232+
}
229233
}
230234

231235
protected CmmnModel getCaseModel(CmmnParseResult parseResult, CaseDefinitionEntity caseDefinition) {
@@ -237,20 +241,6 @@ protected CmmnModel getCaseModel(CmmnParseResult parseResult, CaseDefinitionEnti
237241
return caseModel;
238242
}
239243

240-
protected boolean isManualCorrelationSubscriptionConfiguration(CmmnParseResult parseResult, CaseDefinitionEntity caseDefinition) {
241-
CmmnModel caseModel = getCaseModel(parseResult, caseDefinition);
242-
List<ExtensionElement> correlationCfgExtensions = caseModel.getPrimaryCase().getExtensionElements()
243-
.getOrDefault(CmmnXmlConstants.START_EVENT_CORRELATION_CONFIGURATION, Collections.emptyList());
244-
if (!correlationCfgExtensions.isEmpty()) {
245-
return Objects.equals(correlationCfgExtensions.get(0).getElementText(), CmmnXmlConstants.START_EVENT_CORRELATION_MANUAL);
246-
}
247-
return false;
248-
}
249-
250-
protected String getEventCorrelationKey(Case caseModel) {
251-
return CmmnCorrelationUtil.getCorrelationKey(CmmnXmlConstants.ELEMENT_EVENT_CORRELATION_PARAMETER, CommandContextUtil.getCommandContext(), caseModel);
252-
}
253-
254244
protected void makeCaseDefinitionsConsistentWithPersistedVersions(CmmnParseResult parseResult) {
255245
for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) {
256246
CaseDefinitionEntity persistedCaseDefinition = getPersistedInstanceOfCaseDefinition(caseDefinition);

0 commit comments

Comments
 (0)