const {
moddle,
toBpmnObject,
toBpmnXml,
getElementById,
getElementsByTagName,
manipulateElementsByTagName,
manipulateElementById,
getChildren,
} = require('./util.js');
const {
generateTargetNamespace,
getUserTaskImplementationString,
} = require('./PROCEED-CONSTANTS.js');
const ConstraintParser = require('@proceed/constraint-parser-xml-json');
const constraintParser = new ConstraintParser();
/**
* @module @proceed/bpmn-helper
*/
/**
* Sets id in definitions element to given id, if an id already exists and differs from the new one the old id will be saved in the originalId field
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {string} id - the id we want to set the definitions element to
* @returns {(object|Promise<string>)} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setDefinitionsId(bpmn, id) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Definitions', (definitions) => {
// store the current id as originalId if there is one and we provide a new one
if (id && definitions.id && definitions.id !== id) {
definitions.originalId = definitions.id;
}
definitions.id = id;
});
}
/**
* Sets name in definitions element to given name
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {string} name - the id we want to set the definitions element to
* @returns {(object|Promise<string>)} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setDefinitionsName(bpmn, name) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Definitions', (definitions) => {
definitions.name = `${name}`;
});
}
/**
* Sets name in definitions element to given name
*
* @param {string} bpmn the xml we want to update
* @param {string} id the id we want to set for the process inside the bpmn
* @returns {string} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setProcessId(bpmn, id) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Process', (process) => {
process.id = `${id}`;
});
}
async function setTemplateId(bpmn, id) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Definitions', (definitions) => {
definitions.templateId = `${id}`;
});
}
/**
* Sets targetNamespace in definitions element to https://docs.proceed-labs.org/${id}, keeps existing namespace as originalTargetNamespace
*
* @param {(string|object)} bpmn the process definition as XML string or BPMN-Moddle Object
* @param {string} id the id to be used for the targetNamespace
* @returns {(object|Promise<string>)} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setTargetNamespace(bpmn, id) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Definitions', (definitions) => {
if (id) {
const targetNamespace = generateTargetNamespace(id);
if (definitions.targetNamespace && definitions.targetNamespace !== targetNamespace) {
definitions.originalTargetNamespace = definitions.targetNamespace;
}
definitions.targetNamespace = targetNamespace;
} else {
definitions.targetNamespace = undefined;
}
});
}
/**
* Sets exporter, exporterVersion, expressionLanguage, typeLanguage and needed namespaces on defintions element
* stores the previous values of exporter and exporterVersion if there are any
*
* @param {(string|object)} bpmn the process definition as XML string or BPMN-Moddle Object
* @param {String} exporterName - the exporter name
* @param {String} exporterVersion - the exporter version
* @returns {(object|Promise<string>)} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setStandardDefinitions(bpmn, exporterName, exporterVersion) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Definitions', (definitions) => {
if (definitions.exporter && definitions.exporter !== exporterName) {
definitions.originalExporter = definitions.exporter;
definitions.originalExporterVersion = definitions.exporterVersion;
}
if (definitions.exporterVersion && definitions.exporterVersion !== exporterVersion) {
definitions.originalExporterVersion = definitions.exporterVersion;
}
definitions.exporter = exporterName;
definitions.exporterVersion = exporterVersion;
definitions.expressionLanguage = 'https://ecma-international.org/ecma-262/8.0/';
definitions.typeLanguage =
'https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf';
if (typeof definitions.$attrs != 'object') definitions.$attrs = {};
delete definitions.$attrs['xmlns:bpmn'];
definitions.$attrs['xmlns'] = 'http://www.omg.org/spec/BPMN/20100524/MODEL';
definitions.$attrs['xmlns:proceed'] = 'https://docs.proceed-labs.org/BPMN';
const proceedXSIs = [
'https://docs.proceed-labs.org/BPMN https://docs.proceed-labs.org/xsd/XSD-PROCEED.xsd',
'http://www.omg.org/spec/BPMN/20100524/MODEL https://www.omg.org/spec/BPMN/20100501/BPMN20.xsd',
];
if (typeof definitions.$attrs['xmlns:xsd'] !== 'string') {
definitions.$attrs['xmlns:xsd'] = 'http://www.w3.org/2001/XMLSchema';
}
if (typeof definitions.$attrs['xsi:schemaLocation'] !== 'string') {
definitions.$attrs['xsi:schemaLocation'] = '';
}
if (typeof definitions.creatorEnvironmentId !== 'string') {
definitions.creatorEnvironmentId = '';
}
if (typeof definitions.creatorEnvironmentName !== 'string') {
definitions.creatorEnvironmentName = '';
}
for (const xsi of proceedXSIs) {
if (!definitions.$attrs['xsi:schemaLocation'].includes(xsi)) {
definitions.$attrs['xsi:schemaLocation'] += ' ' + xsi;
}
}
definitions.$attrs['xsi:schemaLocation'] = definitions.$attrs['xsi:schemaLocation'].trim();
});
}
/**
* Sets deployment method of a process
*
* @param {(string|object)} bpmn the process definition as XML string or BPMN-Moddle Object
* @param {string} method the method we want to set (dynamic/static)
* @returns {(object|Promise<string>)} the modified BPMN process as bpmn-moddle object or XML string based on input
*/
async function setDeploymentMethod(bpmn, method) {
return await manipulateElementsByTagName(bpmn, 'bpmn:Process', (process) => {
process.deploymentMethod = method;
});
}
/**
* Sets the 'fileName' and 'implementation' attributes of a UserTask with new values.
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {string} userTaskId - the userTaskId to look for
* @param {string} newFileName - the new value of 'fileName' attribute
* @param {string} [newImplementation] - the new value of 'implementation' attribute; will default to html implementation
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function setUserTaskData(
bpmn,
userTaskId,
newFileName,
newImplementation = getUserTaskImplementationString()
) {
return await manipulateElementById(bpmn, userTaskId, (userTask) => {
userTask.fileName = newFileName;
userTask.implementation = newImplementation;
});
}
/**
* Function that sets the machineInfo of all elements in the given xml with the given machineIds
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {Object[]} machineInfo the machineAddresses and machineIps of all the elements we want to set
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function setMachineInfo(bpmn, machineInfo) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const elementIds = Object.keys(machineInfo);
elementIds.forEach(async (elementId) => {
const element = getElementById(bpmnObj, elementId);
element.machineAddress = machineInfo[elementId].machineAddress;
element.machineId = machineInfo[elementId].machineId;
});
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Adds the given constraint to the given bpmn element
*
* @param {Object} element the bpmn BPMN-Moddle element
* @param {Object} cons object containing the hardConstraints and softConstraints
*/
async function addConstraintsToElement(element, cons) {
if (element) {
let { extensionElements } = element;
if (!extensionElements) {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
element.extensionElements = extensionElements;
}
if (!extensionElements.values) {
extensionElements.values = [];
}
extensionElements.values = extensionElements.values.filter(
(child) => child.$type !== 'proceed:ProcessConstraints'
);
if (cons) {
const { hardConstraints, softConstraints } = cons;
const constraints = { processConstraints: { hardConstraints, softConstraints } };
let constraintXML = constraintParser.fromJsToXml(constraints);
constraintXML = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:extensionElements xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:proceed="https://docs.proceed-labs.org/BPMN">
${constraintXML}
</bpmn2:extensionElements>`;
const constraintObj = await toBpmnObject(constraintXML, 'bpmn:ExtensionElements');
extensionElements.values.push(constraintObj.values[0]);
}
}
}
/**
* Adds the given constraints to the bpmn element with the given id
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {string} elementId
* @param {Object} constraints object containing the hardConstraints and softConstraints
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function addConstraintsToElementById(bpmn, elementId, constraints) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
await addConstraintsToElement(getElementById(bpmnObj, elementId), constraints);
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Add meta information of the called bpmn process to the bpmn file where it's getting called from. This includes a custom namespace in the definitions part,
* an import element as first child of definitions and the calledElement attribute of the call activity bpmn element
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {String} callActivityId The ID of the call activity bpmn element
* @param {String} calledBpmn The bpmn file of the called process
* @param {String} calledProcessLocation The DefinitionId of the calledBpmn. Combination of process name and process id
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function addCallActivityReference(bpmn, callActivityId, calledBpmn, calledProcessLocation) {
// checks if there is already a reference for this call activity and remove it with all related informations (namespace definitions/imports)
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const callActivity = await getElementById(bpmnObj, callActivityId);
if (callActivity.calledElement) {
await removeCallActivityReference(bpmnObj, callActivityId);
}
// Retrieving all necessary informations from the called bpmn
const calledBpmnObject = await toBpmnObject(calledBpmn);
const [calledBpmnDefinitions] = getElementsByTagName(calledBpmnObject, 'bpmn:Definitions');
const [calledProcess] = getElementsByTagName(calledBpmnObject, 'bpmn:Process');
const targetNamespace = calledBpmnDefinitions.targetNamespace;
// Construct namespace in format p+(last 5 chars from the imported namespace), for example 'p33c24'
const nameSpacePrefix = 'p' + targetNamespace.substring(targetNamespace.length - 5);
// Adding the prefixed namespace attribute and the import child-element to the definitions
await manipulateElementsByTagName(bpmnObj, 'bpmn:Definitions', (definitions) => {
definitions.$attrs[`xmlns:${nameSpacePrefix}`] = targetNamespace;
if (!Array.isArray(definitions.imports)) {
definitions.imports = [];
}
// Checks if there is no import element with the same namespace
const processImport = definitions.imports.find(
(element) => element.namespace === targetNamespace
);
if (!processImport) {
let importElement = moddle.create('bpmn:Import');
importElement.namespace = targetNamespace;
importElement.location = calledProcessLocation;
importElement.importType = 'http://www.omg.org/spec/BPMN/20100524/MODEL';
definitions.imports.push(importElement);
} else {
// update process location which might have changed
processImport.location = calledProcessLocation;
}
});
// Adding the desired information to the bpmn call activity element
await manipulateElementById(bpmnObj, callActivityId, (callActivity) => {
callActivity.calledElement = `${nameSpacePrefix}:${calledProcess.id}`;
callActivity.name = calledBpmnDefinitions.name;
});
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Remove the reference to the called process added in {@link addCallActivityReference} but remains the actual bpmn element
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {String} callActivityId The ID of the bpmn element for which the meta information should be removed
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function removeCallActivityReference(bpmn, callActivityId) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const callActivityElement = getElementById(bpmnObj, callActivityId);
if (typeof callActivityElement.calledElement !== 'string') {
return bpmn;
}
// deconstruct 'p33c24:_e069937f-27b6-464b-b397-b88a2599f1b9' to 'p33c24'
const [prefix, processId] = callActivityElement.calledElement.split(':');
// remove the reference values from the call activity
await manipulateElementById(bpmnObj, callActivityId, (callActivity) => {
delete callActivity.calledElement;
callActivity.name = '';
});
const callActivities = getElementsByTagName(bpmnObj, 'bpmn:CallActivity').filter(
(callActivity) => typeof callActivity.calledElement === 'string'
);
// remove import and namespace in definitions if there is no other call activity referencing the same process
if (callActivities.every((callActivity) => !callActivity.calledElement.startsWith(prefix))) {
await manipulateElementsByTagName(bpmnObj, 'bpmn:Definitions', (definitions) => {
const importedNamespace = definitions.$attrs[`xmlns:${prefix}`];
delete definitions.$attrs[`xmlns:${prefix}`];
if (Array.isArray(definitions.imports)) {
definitions.imports = definitions.imports.filter(
(element) => element.namespace !== importedNamespace
);
}
});
}
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Look up the given bpmn document for unused imports/custom namespaces which don't get referenced by a call activity inside this bpmn document.
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function removeUnusedCallActivityReferences(bpmn) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const callActivityElements = getElementsByTagName(bpmnObj, 'bpmn:CallActivity').filter(
(element) => element.calledElement
);
await manipulateElementsByTagName(bpmnObj, 'bpmn:Definitions', (definitions) => {
// there can only be imports and namespaces for non-existent CallActivities if there are actual imports existent and if there are more imports than call activities
if (Array.isArray(definitions.imports)) {
// will be used for comparison later
const importedNamespaces = definitions.imports.map(
(importElement) => importElement.namespace
);
// iterate over all custom namespaces
for (const key in definitions.$attrs) {
// filter namespaces that occur in the imported namespaces
if (importedNamespaces.includes(definitions.$attrs[key])) {
// deconstruct 'xmlns:p33c24' to 'p33c24'
const [_, prefix] = key.split(':');
// checks if there is no actual call activity with this prefix in the calledElement attribute
if (
!callActivityElements.some((callActivityElement) =>
callActivityElement.calledElement.startsWith(prefix)
)
) {
// remove the unused import element
definitions.imports = definitions.imports.filter(
(element) => element.namespace !== definitions.$attrs[key]
);
// remove the unused namespace in definitions
delete definitions.$attrs[key];
}
}
}
}
});
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Adds a documentation element to the first process in the process definition
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {string} description the content for the documentation element
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function addDocumentation(bpmn, description) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const [process] = getElementsByTagName(bpmnObj, 'bpmn:Process');
if (!process) {
return bpmn;
}
addDocumentationToProcessObject(process, description);
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
/**
* Adds documentation to a given process object
*
* @param {Object} processObj
* @param {String} description
*/
async function addDocumentationToProcessObject(processObj, description) {
const docs = processObj.get('documentation');
const documentation = moddle.create('bpmn:Documentation', { text: description || '' });
if (docs.length > 0) {
docs[0] = documentation;
} else {
docs.push(documentation);
}
}
/**
* Adds content of a subprocess into the main process in which the subprocess is used
*
* @param {string} mainProcessBPMN - the process definition as XML string
* @param {string} subprocessBPMN - the content of the subprocess as XML string
* @param {string} subprocessId - ID of the subprocess in which the content should be inserted
*/
async function addSubprocessContentToProcessXML(mainProcessBPMN, subprocessBPMN, subprocessId) {
const mainProcessBpmnObj = await toBpmnObject(mainProcessBPMN);
const subprocessBpmnObj = await toBpmnObject(subprocessBPMN);
const subprocessRootElement = getElementById(subprocessBpmnObj, subprocessId);
const subprocessContent = getChildren(subprocessRootElement).filter(
(element) => element.id && element.id !== subprocessId
);
// Add content of subprocess in subprocess-element of the main process
await manipulateElementById(mainProcessBpmnObj, subprocessId, (subprocessElement) => {
subprocessElement.flowElements = subprocessContent;
subprocessElement.extensionElements = subprocessRootElement.extensionElements;
subprocessElement.name = subprocessRootElement.name;
});
mainProcessBpmnObj.diagrams[0].plane.planeElement =
mainProcessBpmnObj.diagrams[0].plane.planeElement
.filter(
(element) =>
element.bpmnElement &&
!subprocessContent.map((e) => e.id).includes(element.bpmnElement.id)
)
.concat(subprocessBpmnObj.diagrams[0].plane.planeElement);
return toBpmnXml(mainProcessBpmnObj);
}
/**
* Returns the extensionElements entry inside the given element (creates one if there isn't one already)
*
* @param {Object} element the element we want the extensionElements entry of
* @returns {Object} the extensionElements entry
*/
function ensureExtensionElements(element) {
let { extensionElements } = element;
if (!extensionElements) {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
element.extensionElements = extensionElements;
}
return extensionElements;
}
function removeEmptyExtensionElements(element) {
const { extensionElements } = element;
// extensionElements doesn't contain any info => remove
if (extensionElements && (!extensionElements.values || !extensionElements.values.length)) {
delete element.extensionElements;
}
}
/**
* Returns the meta entry inside a given element (creates it if there isn't already one)
*
* @param {Object} element the element we want the meta entry of
* @returns {Object} the meta entry
*/
function ensureMetaElement(element) {
const extensionElements = ensureExtensionElements(element);
let meta = extensionElements.values.find((child) => child.$type == 'proceed:Meta');
if (!meta) {
meta = moddle.create('proceed:Meta');
extensionElements.values.push(meta);
}
return meta;
}
function removeEmptyMetaElement(element) {
const { extensionElements } = element;
if (extensionElements && extensionElements.values) {
const meta = extensionElements.values.find((child) => child.$type == 'proceed:Meta');
if (meta) {
// remove unnecessary property list if it has no entries
if (meta.property && !meta.property.length) {
delete meta.property;
}
// meta element doesn't contain any info => remove
if (!Object.getOwnPropertyNames(meta).some((property) => !property.startsWith('$'))) {
extensionElements.values = extensionElements.values.filter((el) => el !== meta);
}
}
}
}
async function removeColorFromAllElements(bpmn) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
bpmnObj.diagrams.forEach((diagram) => {
diagram.plane.planeElement.forEach((planeElement) => {
planeElement.fill = null;
planeElement.stroke = null;
});
});
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
// elements that are specifically defined in the proceed moddle schema
const specificMetaElements = [
'costsPlanned',
'timePlannedDuration',
'timePlannedOccurrence',
'timePlannedEnd',
'occurrenceProbability',
'orderNumber',
'orderName',
'orderCode',
'customerId',
'customerName',
];
/**
* Changes/Adds an element inside a proceed:Meta element
*
* might be either a specific meta element like proceed:costsPlanned
* or a generic meta value defined inside a proceed:property element
*
* @param {Object} meta the meta element to make changes in
* @param {String} type the type of meta value we want to change
* @param {String|Object} value either a string representing the value inside the final elements body or an already complete element represented by an object
* @param {Object} moddle a bpmn-moddle instance to create elements with
* @returns {undefined|Object} the previous value stored under the given meta type
*/
function setMetaDataInMetaElement(meta, type, value, moddle) {
// remember old value for possible revert
let oldValue;
// add an element defined in the proceed moddle schema
if (specificMetaElements.includes(type)) {
oldValue = meta[type];
if (value) {
if (typeof value === 'string') {
// create new moddle element of the given type with the given value
meta[type] = moddle.create(`proceed:${type}`);
meta[type].value = value;
} else {
// use the predefined moddle element
meta[type] = value;
}
} else {
// remove the current value stored under the given type
meta[type] = undefined;
}
} else {
let properties = meta.property || [];
// add value as generic proceed:property
oldValue = properties.find((property) => property.name === type);
if (value) {
let newProperty;
if (typeof value === 'string') {
// create new proceed:property moddle element with the given type as its name attribute
newProperty = moddle.create(`proceed:property`, { name: type });
newProperty.value = value;
} else {
// use the predefined proceed:property element
newProperty = value;
}
if (oldValue) {
// replace the old property
const index = properties.findIndex((entry) => entry === oldValue);
properties[index] = newProperty;
} else {
// add a new property
properties.push(newProperty);
}
} else {
// remove the property with the given type
properties = properties.filter((property) => property.name !== type);
}
// overwrite the properties array
meta.property = properties;
}
return oldValue;
}
/**
* Updates the Meta Information of an element
*
* @param {(string|object)} bpmn - the process definition as XML string or BPMN-Moddle Object
* @param {String} elId the id of the element to update
* @param {Object} metaValues the meta data values to set
* @returns {(string|object)} the BPMN process as XML string or BPMN-Moddle Object based on input
*/
async function setMetaData(bpmn, elId, metaValues) {
const bpmnObj = typeof bpmn === 'string' ? await toBpmnObject(bpmn) : bpmn;
const element = getElementById(bpmnObj, elId);
const meta = ensureMetaElement(element);
Object.entries(metaValues).forEach(([key, value]) => {
setMetaDataInMetaElement(meta, key, value, moddle);
});
removeEmptyMetaElement(element);
removeEmptyExtensionElements(element);
return typeof bpmn === 'string' ? await toBpmnXml(bpmnObj) : bpmnObj;
}
module.exports = {
setDefinitionsId,
setDefinitionsName,
setProcessId,
setTemplateId,
setTargetNamespace,
setStandardDefinitions,
setDeploymentMethod,
setMachineInfo,
setUserTaskData,
addConstraintsToElementById,
addCallActivityReference,
removeCallActivityReference,
removeUnusedCallActivityReferences,
removeColorFromAllElements,
addDocumentation,
addDocumentationToProcessObject,
addSubprocessContentToProcessXML,
ensureExtensionElements,
removeEmptyExtensionElements,
ensureMetaElement,
removeEmptyMetaElement,
// proceed:Meta related
setMetaData,
setMetaDataInMetaElement,
};