Skip to content

Commit

Permalink
feat: add more data in the internal model for the link events (#2924)
Browse files Browse the repository at this point in the history
Add `sourceIds` & `targetId` properties in the internal model for the
link events.

In the BPMN specification, the link event definition can refer to another
link event definition as either a source or a target.

Since the user of the library doesn't have this information, we
internally calculate the ID of the event associated with this other link
event definition to set as either source or target in
`ShapeBpmnSemantic`.

---------

Co-authored-by: Thomas Bouffard <[email protected]>
  • Loading branch information
csouchet and tbouffard authored Oct 13, 2023
1 parent 7fc889d commit 85c8b20
Show file tree
Hide file tree
Showing 7 changed files with 1,005 additions and 15 deletions.
744 changes: 743 additions & 1 deletion docs/users/architecture/images/architecture/internal-model.drawio

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 58 additions & 7 deletions src/component/parser/json/converter/ProcessConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
} from '../../../../model/bpmn/internal';
import { AssociationFlow, SequenceFlow } from '../../../../model/bpmn/internal/edge/flows';
import ShapeBpmnElement, {
ShapeBpmnIntermediateThrowEvent,
ShapeBpmnIntermediateCatchEvent,
ShapeBpmnActivity,
ShapeBpmnBoundaryEvent,
ShapeBpmnCallActivity,
Expand Down Expand Up @@ -90,6 +92,7 @@ export default class ProcessConverter {
private defaultSequenceFlowIds: string[] = [];
private elementsWithoutParentByProcessId = new Map<string, ShapeBpmnElement[]>();
private callActivitiesCallingProcess = new Map<string, ShapeBpmnElement>();
private eventsByLinkEventDefinition = new Map<RegisteredEventDefinition, ShapeBpmnIntermediateThrowEvent | ShapeBpmnIntermediateCatchEvent>();

constructor(
private convertedElements: ConvertedElements,
Expand All @@ -103,6 +106,7 @@ export default class ProcessConverter {
for (const process of ensureIsArray(processes)) this.assignParentOfProcessElementsCalledByCallActivity(process.id);

this.assignIncomingAndOutgoingIdsFromFlows();
this.assignSourceAndTargetIdsToLinkEvents();
}

private assignParentOfProcessElementsCalledByCallActivity(processId: string): void {
Expand Down Expand Up @@ -139,6 +143,23 @@ export default class ProcessConverter {
}
}

private assignSourceAndTargetIdsToLinkEvents(): void {
const linkEventDefinitions = [...this.eventsByLinkEventDefinition.entries()].filter(([targetEventDefinition]) => targetEventDefinition.id);

for (const [eventDefinition, bpmnEvent] of this.eventsByLinkEventDefinition) {
if (bpmnEvent instanceof ShapeBpmnIntermediateThrowEvent) {
const target = linkEventDefinitions.find(([targetEventDefinition]) => eventDefinition.target === targetEventDefinition.id);
bpmnEvent.targetId = target?.[1]?.id;
} else if (bpmnEvent instanceof ShapeBpmnIntermediateCatchEvent) {
bpmnEvent.sourceIds = linkEventDefinitions
.filter(([sourceEventDefinition]) =>
Array.isArray(eventDefinition.source) ? eventDefinition.source.includes(sourceEventDefinition.id) : eventDefinition.source === sourceEventDefinition.id,
)
.map(([, sourceEvent]) => sourceEvent.id);
}
}
}

private parseProcess(process: TProcess): void {
const processReference = process.id;
const pool = this.convertedElements.findPoolByProcessRef(processReference);
Expand Down Expand Up @@ -251,15 +272,45 @@ export default class ProcessConverter {
}

if (numberOfEventDefinitions == 1) {
const eventDefinitionKind = [...eventDefinitionsByKind.keys()][0];
if (ShapeUtil.isBoundaryEvent(elementKind)) {
return this.buildShapeBpmnBoundaryEvent(bpmnElement as TBoundaryEvent, eventDefinitionKind);
}
if (ShapeUtil.isStartEvent(elementKind)) {
return new ShapeBpmnStartEvent(bpmnElement.id, bpmnElement.name, eventDefinitionKind, parentId, bpmnElement.isInterrupting);
const [eventDefinitionKind, eventDefinitions] = [...eventDefinitionsByKind.entries()][0];

const bpmnEvent = ShapeUtil.isCatchEvent(elementKind)
? this.buildShapeBpmnCatchEvent(bpmnElement as TCatchEvent, elementKind, eventDefinitionKind, parentId)
: this.buildShapeBpmnThrowEvent(bpmnElement as TThrowEvent, elementKind, eventDefinitionKind, parentId);

if (eventDefinitionKind === ShapeBpmnEventDefinitionKind.LINK && (eventDefinitions[0].id || eventDefinitions[0].target || eventDefinitions[0].source)) {
this.eventsByLinkEventDefinition.set(eventDefinitions[0], bpmnEvent);
}
return new ShapeBpmnEvent(bpmnElement.id, bpmnElement.name, elementKind, eventDefinitionKind, parentId);

return bpmnEvent;
}
}

private buildShapeBpmnCatchEvent(
bpmnElement: TCatchEvent,
elementKind: BpmnEventKind,
eventDefinitionKind: ShapeBpmnEventDefinitionKind,
parentId: string,
): ShapeBpmnIntermediateCatchEvent | ShapeBpmnStartEvent | ShapeBpmnBoundaryEvent {
if (ShapeUtil.isBoundaryEvent(elementKind)) {
return this.buildShapeBpmnBoundaryEvent(bpmnElement as TBoundaryEvent, eventDefinitionKind);
}
if (ShapeUtil.isStartEvent(elementKind)) {
return new ShapeBpmnStartEvent(bpmnElement.id, bpmnElement.name, eventDefinitionKind, parentId, bpmnElement.isInterrupting);
}
return new ShapeBpmnIntermediateCatchEvent(bpmnElement.id, bpmnElement.name, eventDefinitionKind, parentId);
}

private buildShapeBpmnThrowEvent(
bpmnElement: TThrowEvent,
elementKind: BpmnEventKind,
eventDefinitionKind: ShapeBpmnEventDefinitionKind,
parentId: string,
): ShapeBpmnIntermediateThrowEvent | ShapeBpmnEvent {
if (ShapeUtil.isIntermediateThrowEvent(elementKind)) {
return new ShapeBpmnIntermediateThrowEvent(bpmnElement.id, bpmnElement.name, eventDefinitionKind, parentId);
}
return new ShapeBpmnEvent(bpmnElement.id, bpmnElement.name, elementKind, eventDefinitionKind, parentId);
}

private buildShapeBpmnBoundaryEvent(bpmnElement: TBoundaryEvent, eventDefinitionKind: ShapeBpmnEventDefinitionKind): ShapeBpmnBoundaryEvent {
Expand Down
22 changes: 22 additions & 0 deletions src/model/bpmn/internal/shape/ShapeBpmnElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,28 @@ export class ShapeBpmnEvent extends ShapeBpmnElement {
}
}

/**
* @internal
*/
export class ShapeBpmnIntermediateCatchEvent extends ShapeBpmnEvent {
sourceIds?: string[] = [];

constructor(id: string, name: string, eventDefinitionKind: ShapeBpmnEventDefinitionKind, parentId: string) {
super(id, name, ShapeBpmnElementKind.EVENT_INTERMEDIATE_CATCH, eventDefinitionKind, parentId);
}
}

/**
* @internal
*/
export class ShapeBpmnIntermediateThrowEvent extends ShapeBpmnEvent {
targetId?: string;

constructor(id: string, name: string, eventDefinitionKind: ShapeBpmnEventDefinitionKind, parentId: string) {
super(id, name, ShapeBpmnElementKind.EVENT_INTERMEDIATE_THROW, eventDefinitionKind, parentId);
}
}

/**
* @internal
*/
Expand Down
12 changes: 12 additions & 0 deletions src/model/bpmn/internal/shape/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export class ShapeUtil {
return ShapeBpmnElementKind.EVENT_START === kind;
}

static isCatchEvent(kind: ShapeBpmnElementKind): boolean {
return ShapeBpmnElementKind.EVENT_INTERMEDIATE_CATCH === kind || ShapeBpmnElementKind.EVENT_BOUNDARY === kind || ShapeBpmnElementKind.EVENT_START === kind;
}

static isIntermediateCatchEvent(kind: ShapeBpmnElementKind): boolean {
return ShapeBpmnElementKind.EVENT_INTERMEDIATE_CATCH === kind;
}

static isIntermediateThrowEvent(kind: ShapeBpmnElementKind): boolean {
return ShapeBpmnElementKind.EVENT_INTERMEDIATE_THROW === kind;
}

static isCallActivity(kind: ShapeBpmnElementKind): boolean {
return ShapeBpmnElementKind.CALL_ACTIVITY === kind;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
Copyright 2023 Bonitasoft S.A.
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.
*/

import { verifyShape } from '../../../helpers/bpmn-model-expect';
import { buildDefinitions, EventDefinitionOn } from '../../../helpers/JsonBuilder';
import { parseJsonAndExpectOnlyFlowNodes } from '../../../helpers/JsonTestUtils';
import { expectedBounds } from '../../../helpers/TestUtils.BpmnJsonParser.event';

import { ShapeBpmnElementKind, ShapeBpmnEventDefinitionKind } from '@lib/model/bpmn/internal';

describe('parse bpmn as json for link events', () => {
describe.each([
['event with eventDefinitionRef attribute', EventDefinitionOn.DEFINITIONS],
['event with eventDefinition object', EventDefinitionOn.EVENT],
] as [string, EventDefinitionOn][])(`%s`, (titleSuffix: string, eventDefinitionOn: EventDefinitionOn) => {
it(`should convert with one source and one target, ${titleSuffix}`, () => {
const json = buildDefinitions({
process: {
event: [
{
id: 'source_id',
bpmnKind: 'intermediateThrowEvent',
eventDefinitionParameter: {
eventDefinitionKind: 'link',
eventDefinitionOn,
target: 'link_target_id', // event definition id of 'target_id'
},
},
{
id: 'target_id',
bpmnKind: 'intermediateCatchEvent',
eventDefinitionParameter: {
eventDefinitionKind: 'link',
eventDefinitionOn,
source: 'link_source_id', // event definition id of 'source_id'
},
},
],
},
});

const model = parseJsonAndExpectOnlyFlowNodes(json, 2);

verifyShape(model.flowNodes[0], {
shapeId: `shape_source_id`,
bpmnElementId: `source_id`,
bounds: expectedBounds,
bpmnElementKind: ShapeBpmnElementKind.EVENT_INTERMEDIATE_THROW,
bpmnElementName: undefined,
eventDefinitionKind: ShapeBpmnEventDefinitionKind.LINK,
targetId: 'target_id',
});
verifyShape(model.flowNodes[1], {
shapeId: `shape_target_id`,
bpmnElementId: `target_id`,
bounds: expectedBounds,
bpmnElementKind: ShapeBpmnElementKind.EVENT_INTERMEDIATE_CATCH,
bpmnElementName: undefined,
eventDefinitionKind: ShapeBpmnEventDefinitionKind.LINK,
sourceIds: ['source_id'],
});
});

it(`should convert with several sources, ${titleSuffix}`, () => {
const json = buildDefinitions({
process: {
event: [
{
id: 'source_1_id',
bpmnKind: 'intermediateThrowEvent',
eventDefinitionParameter: {
eventDefinitionKind: 'link',
eventDefinitionOn,
target: 'link_target_id', // event definition id of 'target_id'
},
},
{
id: 'source_2_id',
bpmnKind: 'intermediateThrowEvent',
eventDefinitionParameter: {
eventDefinitionKind: 'link',
eventDefinitionOn,
target: 'link_target_id', // event definition id of 'target_id'
},
},
{
id: 'target_id',
bpmnKind: 'intermediateCatchEvent',
eventDefinitionParameter: {
eventDefinitionKind: 'link',
eventDefinitionOn,
source: ['link_source_1_id', 'link_source_2_id'], // event definition id of 'source_1_id' & 'source_2_id'
},
},
],
},
});

const model = parseJsonAndExpectOnlyFlowNodes(json, 3);

verifyShape(model.flowNodes[0], {
shapeId: `shape_source_1_id`,
bpmnElementId: `source_1_id`,
bounds: expectedBounds,
bpmnElementKind: ShapeBpmnElementKind.EVENT_INTERMEDIATE_THROW,
bpmnElementName: undefined,
eventDefinitionKind: ShapeBpmnEventDefinitionKind.LINK,
targetId: 'target_id',
});
verifyShape(model.flowNodes[1], {
shapeId: `shape_source_2_id`,
bpmnElementId: `source_2_id`,
bounds: expectedBounds,
bpmnElementKind: ShapeBpmnElementKind.EVENT_INTERMEDIATE_THROW,
bpmnElementName: undefined,
eventDefinitionKind: ShapeBpmnEventDefinitionKind.LINK,
targetId: 'target_id',
});
verifyShape(model.flowNodes[2], {
shapeId: `shape_target_id`,
bpmnElementId: `target_id`,
bounds: expectedBounds,
bpmnElementKind: ShapeBpmnElementKind.EVENT_INTERMEDIATE_CATCH,
bpmnElementName: undefined,
eventDefinitionKind: ShapeBpmnEventDefinitionKind.LINK,
sourceIds: ['source_1_id', 'source_2_id'],
});
});
});
});
30 changes: 25 additions & 5 deletions test/unit/helpers/bpmn-model-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import type { GlobalTaskKind, ShapeBpmnCallActivityKind, ShapeBpmnElementKind, ShapeBpmnEventDefinitionKind } from '@lib/model/bpmn/internal';
import type { GlobalTaskKind, ShapeBpmnCallActivityKind, ShapeBpmnElementKind } from '@lib/model/bpmn/internal';
import type BpmnModel from '@lib/model/bpmn/internal/BpmnModel';
import type { Edge, Waypoint } from '@lib/model/bpmn/internal/edge/edge';
import type Shape from '@lib/model/bpmn/internal/shape/Shape';
import type { ShapeBpmnIntermediateCatchEvent, ShapeBpmnIntermediateThrowEvent } from '@lib/model/bpmn/internal/shape/ShapeBpmnElement';
import type { EdgeExtensions, LabelExtensions, ShapeExtensions } from '@lib/model/bpmn/internal/types';
import type { TProcess } from '@lib/model/bpmn/json/baseElement/rootElement/rootElement';

import { FlowKind, MessageVisibleKind, SequenceFlowKind, ShapeBpmnMarkerKind, ShapeBpmnSubProcessKind } from '@lib/model/bpmn/internal';
import { FlowKind, MessageVisibleKind, SequenceFlowKind, ShapeBpmnMarkerKind, ShapeBpmnSubProcessKind, ShapeBpmnEventDefinitionKind, ShapeUtil } from '@lib/model/bpmn/internal';
import { SequenceFlow } from '@lib/model/bpmn/internal/edge/flows';
import { ShapeBpmnActivity, ShapeBpmnBoundaryEvent, ShapeBpmnCallActivity, ShapeBpmnEvent } from '@lib/model/bpmn/internal/shape/ShapeBpmnElement';

Expand Down Expand Up @@ -51,7 +52,15 @@ export interface ExpectedEventShape extends ExpectedShape {
eventDefinitionKind: ShapeBpmnEventDefinitionKind;
}

export interface ExpectedBoundaryEventShape extends ExpectedEventShape {
export interface ExpectedCatchEventShape extends ExpectedEventShape {
sourceIds?: string[];
}

export interface ExpectedThrowEventShape extends ExpectedEventShape {
targetId?: string;
}

export interface ExpectedBoundaryEventShape extends ExpectedCatchEventShape {
isInterrupting?: boolean;
}

Expand Down Expand Up @@ -94,7 +103,7 @@ export interface ExpectedBounds {

export const verifyShape = (
shape: Shape,
expectedShape: ExpectedShape | ExpectedActivityShape | ExpectedCallActivityShape | ExpectedEventShape | ExpectedBoundaryEventShape,
expectedShape: ExpectedShape | ExpectedActivityShape | ExpectedCallActivityShape | ExpectedCatchEventShape | ExpectedThrowEventShape | ExpectedBoundaryEventShape,
): void => {
expect(shape.id).toEqual(expectedShape.shapeId);
expect(shape.isHorizontal).toEqual(expectedShape.isHorizontal);
Expand Down Expand Up @@ -123,7 +132,18 @@ export const verifyShape = (

if ('eventDefinitionKind' in expectedShape) {
expect(bpmnElement instanceof ShapeBpmnEvent).toBeTruthy();
expect((bpmnElement as ShapeBpmnEvent).eventDefinitionKind).toEqual((expectedShape as ExpectedEventShape).eventDefinitionKind);

const expectedEvent = expectedShape as ExpectedEventShape;
const event = bpmnElement as ShapeBpmnEvent;
expect(event.eventDefinitionKind).toEqual(expectedEvent.eventDefinitionKind);

if (expectedEvent.eventDefinitionKind === ShapeBpmnEventDefinitionKind.LINK) {
if (ShapeUtil.isIntermediateCatchEvent(expectedShape.bpmnElementKind)) {
expect((event as ShapeBpmnIntermediateCatchEvent).sourceIds).toEqual((expectedEvent as ExpectedCatchEventShape).sourceIds ?? []);
} else {
expect((event as ShapeBpmnIntermediateThrowEvent).targetId).toEqual((expectedEvent as ExpectedThrowEventShape).targetId);
}
}
}

if ('isInterrupting' in expectedShape) {
Expand Down

0 comments on commit 85c8b20

Please sign in to comment.