UNPKG

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