BpmnTreeWalker.js 11 KB

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