Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Handle bpmn:DataInputAssociation and bpmn:DataOutputAssociation according to BPMN 2.0 specification #910

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/features/modeling/BpmnFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ BpmnFactory.prototype._needsId = function(element) {
'bpmndi:BPMNEdge',
'bpmndi:BPMNDiagram',
'bpmndi:BPMNPlane',
'bpmn:Property'
'bpmn:Property',
'bpmn:InputOutputSpecification',
'bpmn:DataInput',
'bpmn:InputSet',
'bpmn:DataOutput',
'bpmn:OutputSet'
]);
};

Expand Down
196 changes: 151 additions & 45 deletions lib/features/modeling/behavior/DataInputAssociationBehavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,19 @@ import {
} from 'diagram-js/lib/util/Collections';

import {
find
find,
forEach
} from 'min-dash';

import {
is
} from '../../../util/ModelUtil';

var TARGET_REF_PLACEHOLDER_NAME = '__targetRef_placeholder';


/**
* This behavior makes sure we always set a fake
* DataInputAssociation#targetRef as demanded by the BPMN 2.0
* XSD schema.
*
* The reference is set to a bpmn:Property{ name: '__targetRef_placeholder' }
* which is created on the fly and cleaned up afterwards if not needed
* anymore.
* This behavior makes sure a bpmn:DataInput is created and referenced when a
* bpmn:DataInputAssociation is created. It also makes sure the bpmn:DataInput and
* the reference are removed when a bpmn:InputAssociation is removed.
*
* @param {EventBus} eventBus
* @param {BpmnFactory} bpmnFactory
Expand All @@ -40,66 +35,121 @@ export default function DataInputAssociationBehavior(eventBus, bpmnFactory) {
'connection.delete',
'connection.move',
'connection.reconnectEnd'
], ifDataInputAssociation(fixTargetRef));
], ifDataInputAssociation(updateTargetRef));

this.reverted([
'connection.create',
'connection.delete',
'connection.move',
'connection.reconnectEnd'
], ifDataInputAssociation(fixTargetRef));
], ifDataInputAssociation(updateTargetRef));

/**
* Create and return bpmn:DataInput.
*
* Create bpmn:InputOutputSpecification, dataInputs and inputSets if not
* found.
*
* @param {ModdleElement} element - Element.
*
* @returns {ModdleElement}
*/
function createDataInput(element) {
var ioSpecification = element.get('ioSpecification');

function usesTargetRef(element, targetRef, removedConnection) {
var inputSet, outputSet;

var inputAssociations = element.get('dataInputAssociations');
if (!ioSpecification) {
ioSpecification = bpmnFactory.create('bpmn:InputOutputSpecification', {
dataInputs: [],
inputSets: []
});

return find(inputAssociations, function(association) {
return association !== removedConnection &&
association.targetRef === targetRef;
});
}
element.ioSpecification = ioSpecification;

function getTargetRef(element, create) {
inputSet = bpmnFactory.create('bpmn:InputSet', {
dataInputRefs: [],
name: 'Inputs'
});

var properties = element.get('properties');
inputSet.$parent = ioSpecification;

var targetRefProp = find(properties, function(p) {
return p.name === TARGET_REF_PLACEHOLDER_NAME;
});
collectionAdd(ioSpecification.get('inputSets'), inputSet);

if (!targetRefProp && create) {
targetRefProp = bpmnFactory.create('bpmn:Property', {
name: TARGET_REF_PLACEHOLDER_NAME
outputSet = bpmnFactory.create('bpmn:OutputSet', {
dataOutputRefs: [],
name: 'Outputs'
});

collectionAdd(properties, targetRefProp);
outputSet.$parent = ioSpecification;

collectionAdd(ioSpecification.get('outputSets'), outputSet);
}

return targetRefProp;
}
var dataInput = bpmnFactory.create('bpmn:DataInput');

function cleanupTargetRef(element, connection) {
dataInput.$parent = ioSpecification;

var targetRefProp = getTargetRef(element);
if (!ioSpecification.dataInputs) {
ioSpecification.dataInputs = [];
}

collectionAdd(ioSpecification.get('dataInputs'), dataInput);

if (!ioSpecification.inputSets) {
inputSet = bpmnFactory.create('bpmn:InputSet', {
dataInputRefs: [],
name: 'Inputs'
});

inputSet.$parent = ioSpecification;

collectionAdd(ioSpecification.get('inputSets'), inputSet);
}

inputSet = ioSpecification.get('inputSets')[0];

collectionAdd(inputSet.dataInputRefs, dataInput);

return dataInput;
}

/**
* Remove bpmn:DataInput that is referenced by connection as targetRef from
* bpmn:InputOutputSpecification.
*
* @param {ModdleElement} element - Element.
* @param {ModdleElement} connection - Connection that references
* bpmn:DataInput.
*/
function removeDataInput(element, connection) {
var dataInput = getDataInput(element, connection.targetRef);

if (!targetRefProp) {
if (!dataInput) {
return;
}

if (!usesTargetRef(element, targetRefProp, connection)) {
collectionRemove(element.get('properties'), targetRefProp);
var ioSpecification = element.get('ioSpecification');

if (ioSpecification &&
ioSpecification.dataInputs &&
ioSpecification.inputSets) {

collectionRemove(ioSpecification.dataInputs, dataInput);

collectionRemove(ioSpecification.inputSets[0].dataInputRefs, dataInput);

cleanUpIoSpecification(element);
}
}

/**
* Make sure targetRef is set to a valid property or
* Make sure targetRef is set to a valid bpmn:DataInput or
* `null` if the connection is detached.
*
* @param {Event} event
* @param {Event} event - Event.
*/
function fixTargetRef(event) {

function updateTargetRef(event) {
var context = event.context,
connection = context.connection,
connectionBo = connection.businessObject,
Expand All @@ -111,19 +161,20 @@ export default function DataInputAssociationBehavior(eventBus, bpmnFactory) {
oldTargetBo = oldTarget && oldTarget.businessObject;

var dataAssociation = connection.businessObject,
targetRefProp;
dataInput;

if (oldTargetBo && oldTargetBo !== targetBo) {
cleanupTargetRef(oldTargetBo, connectionBo);
removeDataInput(oldTargetBo, connectionBo);
}

if (newTargetBo && newTargetBo !== targetBo) {
cleanupTargetRef(newTargetBo, connectionBo);
removeDataInput(newTargetBo, connectionBo);
}

if (targetBo) {
targetRefProp = getTargetRef(targetBo, true);
dataAssociation.targetRef = targetRefProp;
dataInput = createDataInput(targetBo, true);

dataAssociation.targetRef = dataInput;
} else {
dataAssociation.targetRef = null;
}
Expand All @@ -137,6 +188,7 @@ DataInputAssociationBehavior.$inject = [

inherits(DataInputAssociationBehavior, CommandInterceptor);

// helpers //////////

/**
* Only call the given function when the event
Expand All @@ -146,7 +198,6 @@ inherits(DataInputAssociationBehavior, CommandInterceptor);
* @return {Function}
*/
function ifDataInputAssociation(fn) {

return function(event) {
var context = event.context,
connection = context.connection;
Expand All @@ -155,4 +206,59 @@ function ifDataInputAssociation(fn) {
return fn(event);
}
};
}

/**
* Get bpmn:DataInput that is is referenced by element as targetRef.
*
* @param {ModdleElement} element - Element.
* @param {ModdleElement} targetRef - Element that is targetRef.
*
* @returns {ModdleElement}
*/
export function getDataInput(element, targetRef) {
var ioSpecification = element.get('ioSpecification');

if (ioSpecification && ioSpecification.dataInputs) {
return find(ioSpecification.dataInputs, function(dataInput) {
return dataInput === targetRef;
});
}
}

/**
* Clean up and remove bpmn:InputOutputSpecification from an element if it's empty.
*
* @param {ModdleElement} element - Element.
*/
function cleanUpIoSpecification(element) {
var ioSpecification = element.get('ioSpecification');

var dataInputs,
dataOutputs,
inputSets,
outputSets;

if (ioSpecification) {
dataInputs = ioSpecification.dataInputs;
dataOutputs = ioSpecification.dataOutputs;
inputSets = ioSpecification.inputSets;
outputSets = ioSpecification.outputSets;

if (dataInputs && !dataInputs.length) {
delete ioSpecification.dataInputs;
}

if (dataOutputs && !dataOutputs.length) {
delete ioSpecification.dataOutputs;
}

if ((!dataInputs || !dataInputs.length) &&
(!dataOutputs || !dataOutputs.length) &&
!inputSets[0].dataInputRefs.length &&
!outputSets[0].dataOutputRefs.length) {

delete element.ioSpecification;
}
}
}