Add new code new version
[sdc.git] / dox-sequence-diagram-ui / src / main / webapp / lib / ecomp / asdc / sequencer / model / Model.js
1 /*!
2  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13  * or implied. See the License for the specific language governing
14  * permissions and limitations under the License.
15  */
16
17 import _merge from 'lodash/merge';
18 // import jsonschema from 'jsonschema';
19
20 import Common from '../common/Common';
21 import Metamodel from './Metamodel';
22
23 /**
24  * A wrapper for a model instance.
25  */
26 export default class Model {
27
28   // ///////////////////////////////////////////////////////////////////////////////////////////////
29
30   /**
31    * Construct model from model JSON. JSON is assumed to be in more or less
32    * the correct structure, but it's OK if it's missing IDs.
33    *
34    * @param json initial JSON; will be updated in situ.
35    * @param metamodel Metaobject definition.
36    */
37   constructor(json, metamodel) {
38
39     if (metamodel) {
40       Common.assertInstanceOf(metamodel, Metamodel);
41     }
42
43     this.metamodel = metamodel || Metamodel.getDefault();
44     Common.assertInstanceOf(this.metamodel, Metamodel);
45
46     this.jsonschema = require('./schema/asdc_sequencer_schema.json');
47     this.templates = {
48       defaultModel: require('./templates/default.model.json'),
49       defaultMetamodel: require('./templates/default.metamodel.json'),
50     };
51
52     this.model = this._preprocess(Common.assertType(json, 'Object'));
53     Common.assertPlainObject(this.model);
54
55     this.renumber();
56
57     this.addLifeline = this.addLifeline.bind(this);
58     this.addMessage = this.addMessage.bind(this);
59     this.renumber = this.renumber.bind(this);
60   }
61
62   // ///////////////////////////////////////////////////////////////////////////////////////////////
63
64   /**
65    * Unwrap to get model object.
66    * @returns {*}
67    */
68   unwrap() {
69     return Common.assertPlainObject(this.model);
70   }
71
72   // ///////////////////////////////////////////////////////////////////////////////////////////////
73
74   /**
75    * Get the metamodel which defines valid states for this model.
76    * @returns Metamodel definition.
77    */
78   getMetamodel() {
79     return Common.assertInstanceOf(this.metamodel, Metamodel);
80   }
81
82   // ///////////////////////////////////////////////////////////////////////////////////////////////
83
84   /**
85    * Find lifeline by its ID.
86    * @param id lifeline ID.
87    * @returns lifeline object, if found.
88    */
89   getLifelineById(id) {
90     for (const lifeline of this.model.diagram.lifelines) {
91       if (lifeline.id === id) {
92         return lifeline;
93       }
94     }
95     return undefined;
96   }
97
98   // ///////////////////////////////////////////////////////////////////////////////////////////////
99
100   /**
101    * Get message by ID.
102    * @param id message ID.
103    * @returns message if matched.
104    */
105   getMessageById(id) {
106     Common.assertNotNull(id);
107     const step = this.getStepByMessageId(id);
108     if (step) {
109       return step.message;
110     }
111     return undefined;
112   }
113
114   // ///////////////////////////////////////////////////////////////////////////////////////////////
115
116   /**
117    * Get step by message ID.
118    * @param id step ID.
119    * @returns step if matched.
120    */
121   getStepByMessageId(id) {
122     Common.assertNotNull(id);
123     for (const step of this.model.diagram.steps) {
124       if (step.message && step.message.id === id) {
125         return step;
126       }
127     }
128     return undefined;
129   }
130
131   // ///////////////////////////////////////////////////////////////////////////////////////////////
132
133   /**
134    * Add message to steps.
135    * @returns {{}}
136    */
137   addMessage(index) {
138     const d = this.model.diagram;
139     const step = {};
140     step.message = {};
141     step.message.id = Model._guid();
142     step.message.name = '[Unnamed Message]';
143     step.message.type = 'request';
144     step.message.from = d.lifelines.length > 0 ? d.lifelines[0].id : -1;
145     step.message.to = d.lifelines.length > 1 ? d.lifelines[1].id : -1;
146     if (index >= 0) {
147       d.steps.splice(index, 0, step);
148     } else {
149       d.steps.push(step);
150     }
151     this.renumber();
152     return step;
153   }
154
155   // ///////////////////////////////////////////////////////////////////////////////////////////////
156
157   /**
158    * Delete message with ID.
159    * @param id to be deleted.
160    */
161   deleteMessageById(id) {
162     Common.assertNotNull(id);
163     const step = this.getStepByMessageId(id);
164     if (step) {
165       const index = this.model.diagram.steps.indexOf(step);
166       if (index !== -1) {
167         this.model.diagram.steps.splice(index, 1);
168       }
169     }
170     this.renumber();
171   }
172
173   // ///////////////////////////////////////////////////////////////////////////////////////////////
174
175   /**
176    * Add lifeline to lifelines.
177    * @param index optional index.
178    * @returns {{}}
179    */
180   addLifeline(index) {
181     const lifeline = {};
182     lifeline.id = Model._guid();
183     lifeline.name = '[Unnamed Lifeline]';
184     if (index >= 0) {
185       this.model.diagram.lifelines.splice(index, 0, lifeline);
186     } else {
187       this.model.diagram.lifelines.push(lifeline);
188     }
189     this.renumber();
190     return lifeline;
191   }
192
193   // ///////////////////////////////////////////////////////////////////////////////////////////////
194
195   /**
196    * Delete lifeline with ID.
197    * @param id to be deleted.
198    */
199   deleteLifelineById(id) {
200     Common.assertNotNull(id);
201     this.deleteStepsByLifelineId(id);
202     const lifeline = this.getLifelineById(id);
203     if (lifeline) {
204       const index = this.model.diagram.lifelines.indexOf(lifeline);
205       if (index !== -1) {
206         this.model.diagram.lifelines.splice(index, 1);
207       }
208     }
209     this.renumber();
210   }
211
212   // ///////////////////////////////////////////////////////////////////////////////////////////////
213
214   /**
215    * Delete all steps corresponding to lifeline.
216    * @param id lifeline ID.
217    */
218   deleteStepsByLifelineId(id) {
219     Common.assertNotNull(id);
220     const steps = this.getStepsByLifelineId(id);
221     for (const step of steps) {
222       this.deleteMessageById(step.message.id);
223     }
224     this.renumber();
225   }
226
227   // ///////////////////////////////////////////////////////////////////////////////////////////////
228
229   /**
230    * Get all steps corresponding to lifeline.
231    * @param id lifeline ID.
232    * @return steps from/to lifeline.
233    */
234   getStepsByLifelineId(id) {
235     Common.assertNotNull(id);
236     const steps = [];
237     for (const step of this.model.diagram.steps) {
238       if (step.message) {
239         if (step.message.from === id || step.message.to === id) {
240           steps.push(step);
241         }
242       }
243     }
244     return steps;
245   }
246
247   // ///////////////////////////////////////////////////////////////////////////////////////////////
248
249   /**
250    * Validate model. Disabled, because we removed the jsonschema dependency.
251    * @returns {Array} of validation errors, if any.
252    */
253   validate() {
254     const errors = [];
255     return errors;
256   }
257
258   // ///////////////////////////////////////////////////////////////////////////////////////////////
259
260   /**
261    * Reorder messages.
262    * @param index message index.
263    * @param afterIndex new (after) index.
264    */
265   reorderMessages(index, afterIndex) {
266     Common.assertType(index, 'Number');
267     Common.assertType(afterIndex, 'Number');
268     const steps = this.model.diagram.steps;
269     const element = steps[index];
270     steps.splice(index, 1);
271     steps.splice(afterIndex, 0, element);
272     this.renumber();
273   }
274
275   // ///////////////////////////////////////////////////////////////////////////////////////////////
276
277   /**
278    * Reorder lifelines.
279    * @param index lifeline index.
280    * @param afterIndex new (after) index.
281    */
282   reorderLifelines(index, afterIndex) {
283     Common.assertType(index, 'Number');
284     Common.assertType(afterIndex, 'Number');
285     const lifelines = this.model.diagram.lifelines;
286     const element = lifelines[index];
287     lifelines.splice(index, 1);
288     lifelines.splice(afterIndex, 0, element);
289     this.renumber();
290   }
291
292   // ///////////////////////////////////////////////////////////////////////////////////////////////
293
294   /**
295    * Renumber lifelines and messages.
296    */
297   renumber() {
298     const modelJSON = this.unwrap();
299     let stepIndex = 1;
300     let lifelineIndex = 1;
301     for (const step of modelJSON.diagram.steps) {
302       if (step.message) {
303         step.message.index = stepIndex++;
304       }
305     }
306     for (const lifeline of modelJSON.diagram.lifelines) {
307       lifeline.index = lifelineIndex++;
308     }
309   }
310
311   // ///////////////////////////////////////////////////////////////////////////////////////////////
312
313   /**
314    * Build a simple, navigable dataset describing fragments.
315    * @returns {{}}, indexed by (stop) message ID, describing fragments.
316    */
317   analyzeFragments() {
318
319     const fData = {};
320
321     let depth = 0;
322     const modelJSON = this.unwrap();
323     const open = [];
324
325     const getData = function g(stop, fragment) {
326       let data = fData[stop];
327       if (!data) {
328         data = { stop, start: [], fragment };
329         fData[stop] = data;
330       }
331       return data;
332     };
333
334     const fragmentsByStart = {};
335     for (const step of modelJSON.diagram.steps) {
336       if (step.message && step.message.fragment) {
337         const message = step.message;
338         const fragment = message.fragment;
339         if (fragment.start) {
340           fragmentsByStart[fragment.start] = fragment;
341           open.push(message.id);
342           depth++;
343         }
344         if (fragment.stop) {
345           if (open.length > 0) {
346             getData(message.id).start.push(open.pop());
347           }
348           depth = Math.max(depth - 1, 0);
349         }
350       }
351     }
352
353     if (open.length > 0) {
354       for (const o of open) {
355         getData(o, fragmentsByStart[o]).start.push(o);
356       }
357     }
358
359     return fData;
360   }
361
362   // ///////////////////////////////////////////////////////////////////////////////////////////////
363
364   /**
365    * Build a simple, navigable dataset describing occurrences.
366    * @returns a map, indexed by lifeline ID, of objects containing {start:[],stop:[],active[]}.
367    * @private
368    */
369   analyzeOccurrences() {
370
371     const oData = {};
372
373     // A few inline functions. They make this method kinda lengthy, but they
374     // reduce clutter in the class and keep it coherent, so it's OK.
375
376     const getDataByLifelineId = function get(lifelineId) {
377       if (!oData[lifelineId]) {
378         oData[lifelineId] = { active: [], start: {}, stop: {} };
379       }
380       return oData[lifelineId];
381     };
382
383     const contains = function contains(array, value) {
384       return (array && (array.indexOf(value) !== -1));
385     };
386
387     const process = function process(message, lifelineId) {
388       const oRule = message.occurrences;
389       if (oRule) {
390
391         const oDataLifeline = getDataByLifelineId(lifelineId);
392         if (oDataLifeline) {
393
394           // Record all starts.
395
396           if (contains(oRule.start, lifelineId)) {
397             oDataLifeline.active.push(message.id);
398             oDataLifeline.start[message.id] = undefined;
399           }
400
401           // Reconcile with stops.
402
403           if (contains(oRule.stop, lifelineId)) {
404             const startMessageId = oDataLifeline.active.pop();
405             oDataLifeline.stop[message.id] = startMessageId;
406             if (startMessageId) {
407               oDataLifeline.start[startMessageId] = message.id;
408             }
409           }
410         }
411       }
412     };
413
414     // Analyze start and end.
415
416     const modelJSON = this.unwrap();
417     for (const step of modelJSON.diagram.steps) {
418       if (step.message) {
419         const message = step.message;
420         if (message.occurrences) {
421           process(message, message.from);
422           process(message, message.to);
423         }
424       }
425     }
426
427     // Reset active. (We used it, but it's not actually for us; it's for keeping
428     // track of active occurrences when rendering the diagram.)
429
430     for (const lifelineId of Object.keys(oData)) {
431       oData[lifelineId].active = [];
432     }
433
434     // Reconcile the start and end (message ID) maps for each lifeline,
435     // finding a "stop" for every start. Default to starting and stopping
436     // on the same message, which is the same as no occurrence.
437
438     for (const lifelineId of Object.keys(oData)) {
439       const lifelineData = oData[lifelineId];
440       for (const startId of Object.keys(lifelineData.start)) {
441         const stopId = lifelineData.start[startId];
442         if (!stopId) {
443           lifelineData.start[startId] = startId;
444           lifelineData.stop[startId] = startId;
445         }
446       }
447     }
448
449     return oData;
450   }
451
452   // ///////////////////////////////////////////////////////////////////////////////////////////////
453   // ///////////////////////////////////////////////////////////////////////////////////////////////
454   // ///////////////////////////////////////////////////////////////////////////////////////////////
455
456   /**
457    * Preprocess model, adding IDs and whatnot.
458    * @param original to be preprocessed.
459    * @returns preprocessed JSON.
460    * @private
461    */
462   _preprocess(original) {
463
464     const json = _merge({}, this.templates.defaultModel, original);
465     const metamodel = this.metamodel.unwrap();
466     if (!json.diagram.metadata.ref) {
467       if (metamodel.diagram.metadata.id) {
468         json.diagram.metadata.ref = metamodel.diagram.metadata.id;
469       } else {
470         json.diagram.metadata.ref = '$';
471       }
472     }
473
474     for (const lifeline of json.diagram.lifelines) {
475       lifeline.id = lifeline.id || lifeline.name;
476     }
477
478     for (const step of json.diagram.steps) {
479       if (step.message) {
480         step.message.id = step.message.id || Model._guid();
481         const occurrences = step.message.occurrences;
482         if (occurrences) {
483           occurrences.start = occurrences.start || [];
484           occurrences.stop = occurrences.stop || [];
485         }
486       }
487     }
488
489     if (!json.diagram.metadata.id || json.diagram.metadata.id === '$') {
490       json.diagram.metadata.id = Model._guid();
491     }
492
493     return json;
494   }
495
496   // ///////////////////////////////////////////////////////////////////////////////////////////////
497
498   /**
499    * Create pseudo-UUID.
500    * @returns {string}
501    * @private
502    */
503   static _guid() {
504     function s4() {
505       return Math.floor((1 + Math.random()) * 0x10000)
506         .toString(16)
507         .substring(1);
508     }
509     return `${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}`;
510   }
511
512 }