2 * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 import _merge from 'lodash/merge';
18 // import jsonschema from 'jsonschema';
20 import Common from '../common/Common';
21 import Metamodel from './Metamodel';
24 * A wrapper for a model instance.
26 export default class Model {
28 // ///////////////////////////////////////////////////////////////////////////////////////////////
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.
34 * @param json initial JSON; will be updated in situ.
35 * @param metamodel Metaobject definition.
37 constructor(json, metamodel) {
40 Common.assertInstanceOf(metamodel, Metamodel);
43 this.metamodel = metamodel || Metamodel.getDefault();
44 Common.assertInstanceOf(this.metamodel, Metamodel);
46 this.jsonschema = require('./schema/asdc_sequencer_schema.json');
48 defaultModel: require('./templates/default.model.json'),
49 defaultMetamodel: require('./templates/default.metamodel.json'),
52 this.model = this._preprocess(Common.assertType(json, 'Object'));
53 Common.assertPlainObject(this.model);
57 this.addLifeline = this.addLifeline.bind(this);
58 this.addMessage = this.addMessage.bind(this);
59 this.renumber = this.renumber.bind(this);
62 // ///////////////////////////////////////////////////////////////////////////////////////////////
65 * Unwrap to get model object.
69 return Common.assertPlainObject(this.model);
72 // ///////////////////////////////////////////////////////////////////////////////////////////////
75 * Get the metamodel which defines valid states for this model.
76 * @returns Metamodel definition.
79 return Common.assertInstanceOf(this.metamodel, Metamodel);
82 // ///////////////////////////////////////////////////////////////////////////////////////////////
85 * Find lifeline by its ID.
86 * @param id lifeline ID.
87 * @returns lifeline object, if found.
90 for (const lifeline of this.model.diagram.lifelines) {
91 if (lifeline.id === id) {
98 // ///////////////////////////////////////////////////////////////////////////////////////////////
102 * @param id message ID.
103 * @returns message if matched.
106 Common.assertNotNull(id);
107 const step = this.getStepByMessageId(id);
114 // ///////////////////////////////////////////////////////////////////////////////////////////////
117 * Get step by message ID.
119 * @returns step if matched.
121 getStepByMessageId(id) {
122 Common.assertNotNull(id);
123 for (const step of this.model.diagram.steps) {
124 if (step.message && step.message.id === id) {
131 // ///////////////////////////////////////////////////////////////////////////////////////////////
134 * Add message to steps.
138 const d = this.model.diagram;
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;
147 d.steps.splice(index, 0, step);
155 // ///////////////////////////////////////////////////////////////////////////////////////////////
158 * Delete message with ID.
159 * @param id to be deleted.
161 deleteMessageById(id) {
162 Common.assertNotNull(id);
163 const step = this.getStepByMessageId(id);
165 const index = this.model.diagram.steps.indexOf(step);
167 this.model.diagram.steps.splice(index, 1);
173 // ///////////////////////////////////////////////////////////////////////////////////////////////
176 * Add lifeline to lifelines.
177 * @param index optional index.
182 lifeline.id = Model._guid();
183 lifeline.name = '[Unnamed Lifeline]';
185 this.model.diagram.lifelines.splice(index, 0, lifeline);
187 this.model.diagram.lifelines.push(lifeline);
193 // ///////////////////////////////////////////////////////////////////////////////////////////////
196 * Delete lifeline with ID.
197 * @param id to be deleted.
199 deleteLifelineById(id) {
200 Common.assertNotNull(id);
201 this.deleteStepsByLifelineId(id);
202 const lifeline = this.getLifelineById(id);
204 const index = this.model.diagram.lifelines.indexOf(lifeline);
206 this.model.diagram.lifelines.splice(index, 1);
212 // ///////////////////////////////////////////////////////////////////////////////////////////////
215 * Delete all steps corresponding to lifeline.
216 * @param id lifeline ID.
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);
227 // ///////////////////////////////////////////////////////////////////////////////////////////////
230 * Get all steps corresponding to lifeline.
231 * @param id lifeline ID.
232 * @return steps from/to lifeline.
234 getStepsByLifelineId(id) {
235 Common.assertNotNull(id);
237 for (const step of this.model.diagram.steps) {
239 if (step.message.from === id || step.message.to === id) {
247 // ///////////////////////////////////////////////////////////////////////////////////////////////
250 * Validate model. Disabled, because we removed the jsonschema dependency.
251 * @returns {Array} of validation errors, if any.
258 // ///////////////////////////////////////////////////////////////////////////////////////////////
262 * @param index message index.
263 * @param afterIndex new (after) index.
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);
275 // ///////////////////////////////////////////////////////////////////////////////////////////////
279 * @param index lifeline index.
280 * @param afterIndex new (after) index.
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);
292 // ///////////////////////////////////////////////////////////////////////////////////////////////
295 * Renumber lifelines and messages.
298 const modelJSON = this.unwrap();
300 let lifelineIndex = 1;
301 for (const step of modelJSON.diagram.steps) {
303 step.message.index = stepIndex++;
306 for (const lifeline of modelJSON.diagram.lifelines) {
307 lifeline.index = lifelineIndex++;
311 // ///////////////////////////////////////////////////////////////////////////////////////////////
314 * Build a simple, navigable dataset describing fragments.
315 * @returns {{}}, indexed by (stop) message ID, describing fragments.
322 const modelJSON = this.unwrap();
325 const getData = function g(stop, fragment) {
326 let data = fData[stop];
328 data = { stop, start: [], fragment };
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);
345 if (open.length > 0) {
346 getData(message.id).start.push(open.pop());
348 depth = Math.max(depth - 1, 0);
353 if (open.length > 0) {
354 for (const o of open) {
355 getData(o, fragmentsByStart[o]).start.push(o);
362 // ///////////////////////////////////////////////////////////////////////////////////////////////
365 * Build a simple, navigable dataset describing occurrences.
366 * @returns a map, indexed by lifeline ID, of objects containing {start:[],stop:[],active[]}.
369 analyzeOccurrences() {
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.
376 const getDataByLifelineId = function get(lifelineId) {
377 if (!oData[lifelineId]) {
378 oData[lifelineId] = { active: [], start: {}, stop: {} };
380 return oData[lifelineId];
383 const contains = function contains(array, value) {
384 return (array && (array.indexOf(value) !== -1));
387 const process = function process(message, lifelineId) {
388 const oRule = message.occurrences;
391 const oDataLifeline = getDataByLifelineId(lifelineId);
394 // Record all starts.
396 if (contains(oRule.start, lifelineId)) {
397 oDataLifeline.active.push(message.id);
398 oDataLifeline.start[message.id] = undefined;
401 // Reconcile with stops.
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;
414 // Analyze start and end.
416 const modelJSON = this.unwrap();
417 for (const step of modelJSON.diagram.steps) {
419 const message = step.message;
420 if (message.occurrences) {
421 process(message, message.from);
422 process(message, message.to);
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.)
430 for (const lifelineId of Object.keys(oData)) {
431 oData[lifelineId].active = [];
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.
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];
443 lifelineData.start[startId] = startId;
444 lifelineData.stop[startId] = startId;
452 // ///////////////////////////////////////////////////////////////////////////////////////////////
453 // ///////////////////////////////////////////////////////////////////////////////////////////////
454 // ///////////////////////////////////////////////////////////////////////////////////////////////
457 * Preprocess model, adding IDs and whatnot.
458 * @param original to be preprocessed.
459 * @returns preprocessed JSON.
462 _preprocess(original) {
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;
470 json.diagram.metadata.ref = '$';
474 for (const lifeline of json.diagram.lifelines) {
475 lifeline.id = lifeline.id || lifeline.name;
478 for (const step of json.diagram.steps) {
480 step.message.id = step.message.id || Model._guid();
481 const occurrences = step.message.occurrences;
483 occurrences.start = occurrences.start || [];
484 occurrences.stop = occurrences.stop || [];
489 if (!json.diagram.metadata.id || json.diagram.metadata.id === '$') {
490 json.diagram.metadata.id = Model._guid();
496 // ///////////////////////////////////////////////////////////////////////////////////////////////
499 * Create pseudo-UUID.
505 return Math.floor((1 + Math.random()) * 0x10000)
509 return `${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}`;