Skip to content

Commit

Permalink
merge: #10714
Browse files Browse the repository at this point in the history
10714: feat(bpmn-model): support escalation events r=remcowesterhoud a=lzgabel

## Description
As a user I can deploy a process definition with escalation event.

## Related issues
closes #10688 

## Definition of Done

Code changes:
* [x] The changes are backwards compatibility with previous versions
* [ ] If it fixes a bug then PRs are created to [backport](https://github.com/camunda/zeebe/compare/stable/0.24...main?expand=1&template=backport_template.md&title=[Backport%200.24]) the fix to the last two minor versions. You can trigger a backport by assigning labels (e.g. `backport stable/1.3`) to the PR, in case that fails you need to create backports manually.

Testing:
* [x] There are unit/integration tests that verify all acceptance criterias of the issue
* [x] New tests are written to ensure backwards compatibility with further versions
* [ ] The behavior is tested manually
* [ ] The change has been verified by a QA run
* [ ] The impact of the changes is verified by a benchmark

Documentation:
* [ ] The documentation is updated (e.g. BPMN reference, configuration, examples, get-started guides, etc.)
* [ ] New content is added to the [release announcement](https://drive.google.com/drive/u/0/folders/1DTIeswnEEq-NggJ25rm2BsDjcCQpDape)
* [ ] If the PR changes how BPMN processes are validated (e.g. support new BPMN element) then the Camunda modeling team should be informed to adjust the BPMN linting.

Please refer to our [review guidelines](https://github.com/camunda/zeebe/wiki/Pull-Requests-and-Code-Reviews#code-review-guidelines).


Co-authored-by: lzgabel <lz19960321lz@gmail.com>
  • Loading branch information
zeebe-bors-camunda[bot] and lzgabel committed Oct 20, 2022
2 parents bacf008 + 6d179d9 commit aa6f2ed
Show file tree
Hide file tree
Showing 15 changed files with 1,134 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ protected Escalation findEscalationForCode(final String escalationCode) {
return escalation;
}

protected EscalationEventDefinition createEmptyEscalationEventDefinition() {
return createInstance(EscalationEventDefinition.class);
}

protected EscalationEventDefinition createEscalationEventDefinition(final String escalationCode) {
final Escalation escalation = findEscalationForCode(escalationCode);
final EscalationEventDefinition escalationEventDefinition =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright © 2017 camunda services GmbH (info@camunda.com)
*
* 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 io.camunda.zeebe.model.bpmn.builder;

import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.Event;

public abstract class AbstractEscalationEventDefinitionBuilder<
B extends AbstractEscalationEventDefinitionBuilder<B>>
extends AbstractRootElementBuilder<B, EscalationEventDefinition> {

public AbstractEscalationEventDefinitionBuilder(
final BpmnModelInstance modelInstance,
final EscalationEventDefinition element,
final Class<?> selfType) {
super(modelInstance, element, selfType);
}

@Override
public B id(final String identifier) {
return super.id(identifier);
}

/** Sets the escalation attribute with escalationCode. */
public B escalationCode(final String escalationCode) {
element.setEscalation(findEscalationForCode(escalationCode));
return myself;
}

/**
* Finishes the building of a escalation event definition.
*
* @param <T>
* @return the parent event builder
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public <T extends AbstractFlowNodeBuilder> T escalationEventDefinitionDone() {
return (T) ((Event) element.getParentElement()).builder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,36 @@ public B escalation(final String escalationCode) {
return myself;
}

/**
* Creates an escalation event definition with an unique id and returns a builder for the
* escalation event definition.
*
* @return the escalation event definition builder object
*/
public EscalationEventDefinitionBuilder escalationEventDefinition(final String id) {
final EscalationEventDefinition escalationEventDefinition =
createEmptyEscalationEventDefinition();
if (id != null) {
escalationEventDefinition.setId(id);
}

element.getEventDefinitions().add(escalationEventDefinition);
return new EscalationEventDefinitionBuilder(modelInstance, escalationEventDefinition);
}

/**
* Creates an escalation event definition and returns a builder for the escalation event
* definition.
*
* @return the escalation event definition builder object
*/
public EscalationEventDefinitionBuilder escalationEventDefinition() {
final EscalationEventDefinition escalationEventDefinition =
createEmptyEscalationEventDefinition();
element.getEventDefinitions().add(escalationEventDefinition);
return new EscalationEventDefinitionBuilder(modelInstance, escalationEventDefinition);
}

public CompensateEventDefinitionBuilder compensateEventDefinition() {
return compensateEventDefinition(null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright © 2017 camunda services GmbH (info@camunda.com)
*
* 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 io.camunda.zeebe.model.bpmn.builder;

import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;

public class EscalationEventDefinitionBuilder
extends AbstractEscalationEventDefinitionBuilder<EscalationEventDefinitionBuilder> {

public EscalationEventDefinitionBuilder(
final BpmnModelInstance modelInstance, final EscalationEventDefinition element) {
super(modelInstance, element, EscalationEventDefinitionBuilder.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@

import io.camunda.zeebe.model.bpmn.instance.Activity;
import io.camunda.zeebe.model.bpmn.instance.BoundaryEvent;
import io.camunda.zeebe.model.bpmn.instance.CallActivity;
import io.camunda.zeebe.model.bpmn.instance.Error;
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.Escalation;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.IntermediateCatchEvent;
import io.camunda.zeebe.model.bpmn.instance.IntermediateThrowEvent;
Expand All @@ -36,7 +39,10 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -48,7 +54,14 @@ public class ModelUtil {

private static final List<Class<? extends EventDefinition>> NON_INTERRUPTING_EVENT_DEFINITIONS =
Arrays.asList(
MessageEventDefinition.class, TimerEventDefinition.class, SignalEventDefinition.class);
MessageEventDefinition.class,
TimerEventDefinition.class,
SignalEventDefinition.class,
EscalationEventDefinition.class);

private static final List<Class<? extends Activity>>
ESCALATION_BOUNDARY_EVENT_SUPPORTED_ACTIVITIES =
Arrays.asList(SubProcess.class, CallActivity.class);

public static List<EventDefinition> getEventDefinitionsForBoundaryEvents(final Activity element) {
return element.getBoundaryEvents().stream()
Expand Down Expand Up @@ -109,6 +122,7 @@ public static void verifyNoDuplicatedBoundaryEvents(
final List<EventDefinition> definitions = getEventDefinitionsForBoundaryEvents(activity);

verifyNoDuplicatedEventDefinition(definitions, errorCollector);
verifyNoDuplicatedEscalationHandler(definitions, errorCollector);
}

public static void verifyNoDuplicateSignalStartEvents(
Expand Down Expand Up @@ -157,8 +171,12 @@ public static void verifyEventDefinition(
boundaryEvent
.getEventDefinitions()
.forEach(
definition ->
verifyEventDefinition(definition, boundaryEvent.cancelActivity(), errorCollector));
definition -> {
if (definition instanceof EscalationEventDefinition) {
verifyEscalationBoundaryEvent(boundaryEvent, errorCollector);
}
verifyEventDefinition(definition, boundaryEvent.cancelActivity(), errorCollector);
});
}

public static void verifyEventDefinition(
Expand All @@ -177,6 +195,7 @@ public static void verifyNoDuplicatedEventSubprocesses(
final List<EventDefinition> definitions = getEventDefinitionsForEventSubprocesses(element);

verifyNoDuplicatedEventDefinition(definitions, errorCollector);
verifyNoDuplicatedEscalationHandler(definitions, errorCollector);
}

public static void verifyNoDuplicatedEventDefinition(
Expand Down Expand Up @@ -220,6 +239,56 @@ public static void verifyNoDuplicatedEventDefinition(
getDuplicatedEntries(linkNames).map(ModelUtil::duplicatedLinkNames).forEach(errorCollector);
}

private static void verifyNoDuplicatedEscalationHandler(
final List<EventDefinition> definitions, final Consumer<String> errorCollector) {
final List<Escalation> escalations =
getEventDefinition(definitions, EscalationEventDefinition.class)
.map(EscalationEventDefinition::getEscalation)
.collect(Collectors.toList());

if (escalations.isEmpty()) {
return;
}

final long definitionWithoutEscalationCount =
escalations.stream().filter(Objects::isNull).count();

if (definitionWithoutEscalationCount > 1) {
errorCollector.accept(
"The same scope can not contain more than one escalation catch event without"
+ " escalation code. An escalation catch event without escalation code catches"
+ " all escalations.");
}

final Map<Optional<String>, Long> escalationCodeOccurrences =
escalations.stream()
.filter(Objects::nonNull)
.map(escalation -> Optional.ofNullable(escalation.getEscalationCode()))
.collect(groupingBy(escalationCode -> escalationCode, counting()));

if (definitionWithoutEscalationCount >= 1 && !escalationCodeOccurrences.isEmpty()) {
errorCollector.accept(
"The same scope can not contain an escalation catch event without escalation code "
+ "and another one with escalation code. An escalation catch event without "
+ "escalation code catches all escalations.");
}

escalationCodeOccurrences.forEach(
(escalationCode, occurrences) -> {
if (occurrences > 1) {
errorCollector.accept(
escalationCode.isPresent()
? String.format(
"Multiple escalation catch events with the same escalation code '%s' are "
+ "not supported on the same scope.",
escalationCode.get())
: "The same scope can not contain more than one escalation catch event without"
+ " escalation code. An escalation catch event without escalation code catches"
+ " all escalations.");
}
});
}

public static <T extends EventDefinition> Stream<T> getEventDefinition(
final Collection<? extends EventDefinition> collection, final Class<T> type) {
return collection.stream().filter(type::isInstance).map(type::cast);
Expand Down Expand Up @@ -276,4 +345,13 @@ private static void verifyEventDefinition(
}
}
}

private static void verifyEscalationBoundaryEvent(
final BoundaryEvent element, final Consumer<String> errorCollector) {
if (ESCALATION_BOUNDARY_EVENT_SUPPORTED_ACTIVITIES.stream()
.noneMatch(activity -> activity.isInstance(element.getAttachedTo()))) {
errorCollector.accept(
"An escalation boundary event should only be attached to a subprocess, or a call activity.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.camunda.zeebe.model.bpmn.instance.BoundaryEvent;
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.SignalEventDefinition;
Expand All @@ -35,7 +36,8 @@ public class BoundaryEventValidator implements ModelElementValidator<BoundaryEve
TimerEventDefinition.class,
MessageEventDefinition.class,
ErrorEventDefinition.class,
SignalEventDefinition.class);
SignalEventDefinition.class,
EscalationEventDefinition.class);

@Override
public Class<BoundaryEvent> getElementType() {
Expand Down Expand Up @@ -72,7 +74,7 @@ private void validateEventDefinition(
def -> {
if (SUPPORTED_EVENT_DEFINITIONS.stream().noneMatch(type -> type.isInstance(def))) {
validationResultCollector.addError(
0, "Boundary events must be one of: timer, message, error, signal");
0, "Boundary events must be one of: timer, message, error, signal, escalation");
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.camunda.zeebe.model.bpmn.instance.EndEvent;
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.SignalEventDefinition;
Expand All @@ -34,7 +35,8 @@ public class EndEventValidator implements ModelElementValidator<EndEvent> {
ErrorEventDefinition.class,
MessageEventDefinition.class,
TerminateEventDefinition.class,
SignalEventDefinition.class);
SignalEventDefinition.class,
EscalationEventDefinition.class);

@Override
public Class<EndEvent> getElementType() {
Expand Down Expand Up @@ -65,7 +67,8 @@ private void validateEventDefinition(
def -> {
if (SUPPORTED_EVENT_DEFINITIONS.stream().noneMatch(type -> type.isInstance(def))) {
validationResultCollector.addError(
0, "End events must be one of: none, error, message, terminate, or signal");
0,
"End events must be one of: none, error, message, terminate, signal, or escalation");
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright © 2017 camunda services GmbH (info@camunda.com)
*
* 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 io.camunda.zeebe.model.bpmn.validation.zeebe;

import io.camunda.zeebe.model.bpmn.instance.EndEvent;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.IntermediateThrowEvent;
import org.camunda.bpm.model.xml.instance.ModelElementInstance;
import org.camunda.bpm.model.xml.validation.ModelElementValidator;
import org.camunda.bpm.model.xml.validation.ValidationResultCollector;

public class EscalationEventDefinitionValidator
implements ModelElementValidator<EscalationEventDefinition> {

@Override
public Class<EscalationEventDefinition> getElementType() {
return EscalationEventDefinition.class;
}

@Override
public void validate(
final EscalationEventDefinition element,
final ValidationResultCollector validationResultCollector) {

if (isEscalationThrowEvent(element) && element.getEscalation() == null) {
validationResultCollector.addError(0, "Must reference an escalation");
}
}

private boolean isEscalationThrowEvent(final EscalationEventDefinition element) {
final ModelElementInstance parentElement = element.getParentElement();
return parentElement instanceof IntermediateThrowEvent || parentElement instanceof EndEvent;
}
}

0 comments on commit aa6f2ed

Please sign in to comment.