BpmnTreeWalker.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import {
  2. filter,
  3. find,
  4. forEach
  5. } from 'min-dash';
  6. import {
  7. elementToString
  8. } from './Util';
  9. import {
  10. ensureCompatDiRef
  11. } from '../util/CompatibilityUtil';
  12. /**
  13. * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate
  14. *
  15. * @typedef {import('../model/Types').ModdleElement} ModdleElement
  16. */
  17. /**
  18. * Returns true if an element is of the given meta-model type.
  19. *
  20. * @param {ModdleElement} element
  21. * @param {string} type
  22. *
  23. * @return {boolean}
  24. */
  25. function is(element, type) {
  26. return element.$instanceOf(type);
  27. }
  28. /**
  29. * Find a suitable display candidate for definitions where the DI does not
  30. * correctly specify one.
  31. *
  32. * @param {ModdleElement} definitions
  33. *
  34. * @return {ModdleElement}
  35. */
  36. function findDisplayCandidate(definitions) {
  37. return find(definitions.rootElements, function(e) {
  38. return is(e, 'bpmn:Process') || is(e, 'bpmn:Collaboration');
  39. });
  40. }
  41. /**
  42. * @param {Record<'element' | 'root' | 'error', Function>} handler
  43. * @param {Translate} translate
  44. */
  45. export default function BpmnTreeWalker(handler, translate) {
  46. // list of containers already walked
  47. var handledElements = {};
  48. // list of elements to handle deferred to ensure
  49. // prerequisites are drawn
  50. var deferred = [];
  51. var diMap = {};
  52. // Helpers //////////////////////
  53. function contextual(fn, ctx) {
  54. return function(e) {
  55. fn(e, ctx);
  56. };
  57. }
  58. function handled(element) {
  59. handledElements[element.id] = element;
  60. }
  61. function isHandled(element) {
  62. return handledElements[element.id];
  63. }
  64. function visit(element, ctx) {
  65. var gfx = element.gfx;
  66. // avoid multiple rendering of elements
  67. if (gfx) {
  68. throw new Error(
  69. translate('already rendered {element}', { element: elementToString(element) })
  70. );
  71. }
  72. // call handler
  73. return handler.element(element, diMap[element.id], ctx);
  74. }
  75. function visitRoot(element, diagram) {
  76. return handler.root(element, diMap[element.id], diagram);
  77. }
  78. function visitIfDi(element, ctx) {
  79. try {
  80. var gfx = diMap[element.id] && visit(element, ctx);
  81. handled(element);
  82. return gfx;
  83. } catch (e) {
  84. logError(e.message, { element: element, error: e });
  85. console.error(translate('failed to import {element}', { element: elementToString(element) }));
  86. console.error(e);
  87. }
  88. }
  89. function logError(message, context) {
  90. handler.error(message, context);
  91. }
  92. // DI handling //////////////////////
  93. var registerDi = this.registerDi = function registerDi(di) {
  94. var bpmnElement = di.bpmnElement;
  95. if (bpmnElement) {
  96. if (diMap[bpmnElement.id]) {
  97. logError(
  98. translate('multiple DI elements defined for {element}', {
  99. element: elementToString(bpmnElement)
  100. }),
  101. { element: bpmnElement }
  102. );
  103. } else {
  104. diMap[bpmnElement.id] = di;
  105. ensureCompatDiRef(bpmnElement);
  106. }
  107. } else {
  108. logError(
  109. translate('no bpmnElement referenced in {element}', {
  110. element: elementToString(di)
  111. }),
  112. { element: di }
  113. );
  114. }
  115. };
  116. function handleDiagram(diagram) {
  117. handlePlane(diagram.plane);
  118. }
  119. function handlePlane(plane) {
  120. registerDi(plane);
  121. forEach(plane.planeElement, handlePlaneElement);
  122. }
  123. function handlePlaneElement(planeElement) {
  124. registerDi(planeElement);
  125. }
  126. // Semantic handling //////////////////////
  127. /**
  128. * Handle definitions and return the rendered diagram (if any).
  129. *
  130. * @param {ModdleElement} definitions to walk and import
  131. * @param {ModdleElement} [diagram] specific diagram to import and display
  132. *
  133. * @throws {Error} if no diagram to display could be found
  134. */
  135. this.handleDefinitions = function handleDefinitions(definitions, diagram) {
  136. // make sure we walk the correct bpmnElement
  137. var diagrams = definitions.diagrams;
  138. if (diagram && diagrams.indexOf(diagram) === -1) {
  139. throw new Error(translate('diagram not part of bpmn:Definitions'));
  140. }
  141. if (!diagram && diagrams && diagrams.length) {
  142. diagram = diagrams[0];
  143. }
  144. // no diagram -> nothing to import
  145. if (!diagram) {
  146. throw new Error(translate('no diagram to display'));
  147. }
  148. // load DI from selected diagram only
  149. diMap = {};
  150. handleDiagram(diagram);
  151. var plane = diagram.plane;
  152. if (!plane) {
  153. throw new Error(translate(
  154. 'no plane for {element}',
  155. { element: elementToString(diagram) }
  156. ));
  157. }
  158. var rootElement = plane.bpmnElement;
  159. // ensure we default to a suitable display candidate (process or collaboration),
  160. // even if non is specified in DI
  161. if (!rootElement) {
  162. rootElement = findDisplayCandidate(definitions);
  163. if (!rootElement) {
  164. throw new Error(translate('no process or collaboration to display'));
  165. } else {
  166. logError(
  167. translate('correcting missing bpmnElement on {plane} to {rootElement}', {
  168. plane: elementToString(plane),
  169. rootElement: elementToString(rootElement)
  170. })
  171. );
  172. // correct DI on the fly
  173. plane.bpmnElement = rootElement;
  174. registerDi(plane);
  175. }
  176. }
  177. var ctx = visitRoot(rootElement, plane);
  178. if (is(rootElement, 'bpmn:Process') || is(rootElement, 'bpmn:SubProcess')) {
  179. handleProcess(rootElement, ctx);
  180. } else if (is(rootElement, 'bpmn:Collaboration')) {
  181. handleCollaboration(rootElement, ctx);
  182. // force drawing of everything not yet drawn that is part of the target DI
  183. handleUnhandledProcesses(definitions.rootElements, ctx);
  184. } else {
  185. throw new Error(
  186. translate('unsupported bpmnElement for {plane}: {rootElement}', {
  187. plane: elementToString(plane),
  188. rootElement: elementToString(rootElement)
  189. })
  190. );
  191. }
  192. // handle all deferred elements
  193. handleDeferred(deferred);
  194. };
  195. var handleDeferred = this.handleDeferred = function handleDeferred() {
  196. var fn;
  197. // drain deferred until empty
  198. while (deferred.length) {
  199. fn = deferred.shift();
  200. fn();
  201. }
  202. };
  203. function handleProcess(process, context) {
  204. handleFlowElementsContainer(process, context);
  205. handleIoSpecification(process.ioSpecification, context);
  206. handleArtifacts(process.artifacts, context);
  207. // log process handled
  208. handled(process);
  209. }
  210. function handleUnhandledProcesses(rootElements, ctx) {
  211. // walk through all processes that have not yet been drawn and draw them
  212. // if they contain lanes with DI information.
  213. // we do this to pass the free-floating lane test cases in the MIWG test suite
  214. var processes = filter(rootElements, function(e) {
  215. return !isHandled(e) && is(e, 'bpmn:Process') && e.laneSets;
  216. });
  217. processes.forEach(contextual(handleProcess, ctx));
  218. }
  219. function handleMessageFlow(messageFlow, context) {
  220. visitIfDi(messageFlow, context);
  221. }
  222. function handleMessageFlows(messageFlows, context) {
  223. forEach(messageFlows, contextual(handleMessageFlow, context));
  224. }
  225. function handleDataAssociation(association, context) {
  226. visitIfDi(association, context);
  227. }
  228. function handleDataInput(dataInput, context) {
  229. visitIfDi(dataInput, context);
  230. }
  231. function handleDataOutput(dataOutput, context) {
  232. visitIfDi(dataOutput, context);
  233. }
  234. function handleArtifact(artifact, context) {
  235. // bpmn:TextAnnotation
  236. // bpmn:Group
  237. // bpmn:Association
  238. visitIfDi(artifact, context);
  239. }
  240. function handleArtifacts(artifacts, context) {
  241. forEach(artifacts, function(e) {
  242. if (is(e, 'bpmn:Association')) {
  243. deferred.push(function() {
  244. handleArtifact(e, context);
  245. });
  246. } else {
  247. handleArtifact(e, context);
  248. }
  249. });
  250. }
  251. function handleIoSpecification(ioSpecification, context) {
  252. if (!ioSpecification) {
  253. return;
  254. }
  255. forEach(ioSpecification.dataInputs, contextual(handleDataInput, context));
  256. forEach(ioSpecification.dataOutputs, contextual(handleDataOutput, context));
  257. }
  258. var handleSubProcess = this.handleSubProcess = function handleSubProcess(subProcess, context) {
  259. handleFlowElementsContainer(subProcess, context);
  260. handleArtifacts(subProcess.artifacts, context);
  261. };
  262. function handleFlowNode(flowNode, context) {
  263. var childCtx = visitIfDi(flowNode, context);
  264. if (is(flowNode, 'bpmn:SubProcess')) {
  265. handleSubProcess(flowNode, childCtx || context);
  266. }
  267. if (is(flowNode, 'bpmn:Activity')) {
  268. handleIoSpecification(flowNode.ioSpecification, context);
  269. }
  270. // defer handling of associations
  271. // affected types:
  272. //
  273. // * bpmn:Activity
  274. // * bpmn:ThrowEvent
  275. // * bpmn:CatchEvent
  276. //
  277. deferred.push(function() {
  278. forEach(flowNode.dataInputAssociations, contextual(handleDataAssociation, context));
  279. forEach(flowNode.dataOutputAssociations, contextual(handleDataAssociation, context));
  280. });
  281. }
  282. function handleSequenceFlow(sequenceFlow, context) {
  283. visitIfDi(sequenceFlow, context);
  284. }
  285. function handleDataElement(dataObject, context) {
  286. visitIfDi(dataObject, context);
  287. }
  288. function handleLane(lane, context) {
  289. deferred.push(function() {
  290. var newContext = visitIfDi(lane, context);
  291. if (lane.childLaneSet) {
  292. handleLaneSet(lane.childLaneSet, newContext || context);
  293. }
  294. wireFlowNodeRefs(lane);
  295. });
  296. }
  297. function handleLaneSet(laneSet, context) {
  298. forEach(laneSet.lanes, contextual(handleLane, context));
  299. }
  300. function handleLaneSets(laneSets, context) {
  301. forEach(laneSets, contextual(handleLaneSet, context));
  302. }
  303. function handleFlowElementsContainer(container, context) {
  304. handleFlowElements(container.flowElements, context);
  305. if (container.laneSets) {
  306. handleLaneSets(container.laneSets, context);
  307. }
  308. }
  309. function handleFlowElements(flowElements, context) {
  310. forEach(flowElements, function(e) {
  311. if (is(e, 'bpmn:SequenceFlow')) {
  312. deferred.push(function() {
  313. handleSequenceFlow(e, context);
  314. });
  315. } else if (is(e, 'bpmn:BoundaryEvent')) {
  316. deferred.unshift(function() {
  317. handleFlowNode(e, context);
  318. });
  319. } else if (is(e, 'bpmn:FlowNode')) {
  320. handleFlowNode(e, context);
  321. } else if (is(e, 'bpmn:DataObject')) {
  322. // SKIP (assume correct referencing via DataObjectReference)
  323. } else if (is(e, 'bpmn:DataStoreReference')) {
  324. handleDataElement(e, context);
  325. } else if (is(e, 'bpmn:DataObjectReference')) {
  326. handleDataElement(e, context);
  327. } else {
  328. logError(
  329. translate('unrecognized flowElement {element} in context {context}', {
  330. element: elementToString(e),
  331. context: (context ? elementToString(context.businessObject) : 'null')
  332. }),
  333. { element: e, context: context }
  334. );
  335. }
  336. });
  337. }
  338. function handleParticipant(participant, context) {
  339. var newCtx = visitIfDi(participant, context);
  340. var process = participant.processRef;
  341. if (process) {
  342. handleProcess(process, newCtx || context);
  343. }
  344. }
  345. function handleCollaboration(collaboration, context) {
  346. forEach(collaboration.participants, contextual(handleParticipant, context));
  347. handleArtifacts(collaboration.artifacts, context);
  348. // handle message flows latest in the process
  349. deferred.push(function() {
  350. handleMessageFlows(collaboration.messageFlows, context);
  351. });
  352. }
  353. function wireFlowNodeRefs(lane) {
  354. // wire the virtual flowNodeRefs <-> relationship
  355. forEach(lane.flowNodeRef, function(flowNode) {
  356. var lanes = flowNode.get('lanes');
  357. if (lanes) {
  358. lanes.push(lane);
  359. }
  360. });
  361. }
  362. }