/* This file is part of AToMPM - A Tool for Multi-Paradigm Modelling * Copyright 2011 by the AToMPM team and licensed under the LGPL * See COPYING.lesser and README.md in the root of this project for full details */ /* NOTES: atom3 supported pre/post actions and constraints for the 'SAVE' EVENT... this never really made any sense (e.g., the user could be prevented from saving and, technically, the effects of post-actions were never saved)... atom3 supported 'save' events as a hack to enable forcing mm validation... in atompm, such validation is carried out by _mmmk.validateModel (which clients can 'call') and thus, we do no support 'save' events... */ const _utils = require('./utils'); const _util = require("util"); const _mt = require("./libmt"); const _styleinfo = require('./styleinfo'); module.exports = { /********************************* GLOBALS *********************************/ 'metamodels':{}, 'model':{'nodes':{},'edges':[],'metamodels':[]}, 'name':'', 'next_id':0, /********************************* ENV SETUP *******************************/ /* produce a bundle of internal state variables sufficient to fully clone this instance OR use a provided bundle to overwrite this instance's internal state */ 'clone' : function(clone) { if( clone ) { this.metamodels = clone.metamodels; this.model = clone.model; this.name = clone.name; this.next_id = clone.next_id; this.journal = clone.journal; this.journalIndex = clone.journalIndex; this.undoredoJournal = clone.undoredoJournal; } else return _utils.clone( {'metamodels': this.metamodels, 'model': this.model, 'name': this.name, 'next_id': this.next_id, 'journal': this.journal, 'journalIndex': this.journalIndex, 'undoredoJournal': this.undoredoJournal}); }, /* load a model into this.model 0. create step-checkpoint 1. make sure all required metamodels are loaded 2. if 'insert' is specified, a) append 'model' to this.model (via __resetm__) 2. otherwise, load 'model' into this.model and 'name' into this.name (via __resetm__) */ 'loadModel' : function(name,model,insert) { this.__setStepCheckpoint(); var new_model = eval('('+ model +')'); for( var i in new_model.metamodels ) if( this.metamodels[new_model.metamodels[i]] == undefined ) return {'$err':'metamodel not loaded :: '+new_model.metamodels[i]}; this.__resetm__(name,model,insert); return {'changelog':this.__changelog()}; }, /* load a metamodel 0. create a step-checkpoint 1. load metamodel into this.model.metamodels and this.metamodels (via __loadmm__) */ 'loadMetamodel' : function(name,mm) { this.__setStepCheckpoint(); this.__loadmm__(name,mm); return {'changelog':this.__changelog()}; }, /* unload a metamodel and delete all entities from that metamodel 0. create a step-checkpoint 1. deletes nodes from specified metamodel 2. delete edges where deleted nodes appear 3. remove metamodel from this.model.metamodels and this.metamodels (via __dumpmm__) */ 'unloadMetamodel' : function(name) { this.__setStepCheckpoint(); for( var i=0; i= card_into['max'] ) return {'$err':'maximum outbound multiplicity reached for '+t1+' ('+id1+') and type '+into}; if( edge['dest'] == id2 && this.__getType(this.model.nodes[edge['src']]['$type']) == from && ++num_toid2 >= card_from['max'] ) return {'$err':'maximum inbound multiplicity reached for '+t2+' ('+id2+') and type '+from}; } if( t1 == tc || t2 == tc ) { var connectorId = (t1 == tc ? id1 : id2), err = this.__crudOp( metamodel, ['connect'], [id1,id2], '__connectCN', {'id1':id1, 'id2':id2, 'connectorId':connectorId}); return err || {'id':connectorId, 'changelog':this.__changelog()}; } else { var err = this.__crudOp( metamodel, ['create'], [this.next_id], '__connectNN', {'id1':id1, 'id2':id2, 'connectorType':connectorType, 'attrs':attrs}); return err || {'id':this.next_id++, 'changelog':this.__changelog()}; } }, /* create an instance of fulltype __create: 1. create [default] instance using metamodel [and possibly specified attrs] + init $type 2. add to current model nodes [3. if fulltype is a connectorType, create edges between node id1 and new instance and between new instance and node id2 create: 0. create a step-checkpoint 1. wrap __create in crudOp 2. return err or new instance id */ '__create' : function(args/*fulltype,attrs,[,id1,id2]*/) { var metamodel = this.__getMetamodel(args.fulltype), type = this.__getType(args.fulltype), typeAttrs = this.metamodels[metamodel]['types'][type], new_node = {}; if( typeAttrs == undefined ) return {'$err':'can not create instance of unknown type :: '+args.fulltype}; typeAttrs.forEach( function(attr) { var val = (args.attrs && attr['name'] in args.attrs ? args.attrs[attr['name']] : attr['default']); new_node[attr['name']] = {'type':attr['type'], 'value':(typeof attr['default'] == 'object' ? _utils.clone(val) : val)}; }); new_node['$type'] = args.fulltype; this.__mknode__(this.next_id,new_node); if( args.id1 != undefined ) { this.__mkedge__(args.id1,String(this.next_id)); this.__mkedge__(String(this.next_id),args.id2); } }, 'create' : function(fulltype,attrs) { this.__setStepCheckpoint(); var err = this.__crudOp( this.__getMetamodel(fulltype), ['create'], [this.next_id], '__create', {'fulltype':fulltype, 'attrs':attrs}); return err || {'id':this.next_id++, 'changelog':this.__changelog()}; }, /* delete the specified node (and appropriate edges and/or connectors) __delete: 1. determine specified node's neighbors 2. if specified node is a connector (neighbors are non-connectors), a) run pre-disconnect constraints and actions and on its neighbors b) delete it and all appropriate edges (via __deleteConnector) c) run post-disconnect constraints and actions and on its neighbors 2. if specified node is not a connector (neighbors are connectors), a) recursively run __delete on each of its neighbors b) delete it __deleteConnector: 1. delete all appropriate edges then delete node delete: 0. create a step-checkpoint 1. wrap __delete in crudOp 2. return err or nothing */ '__delete' : function(args/*id*/) { var id = args.id, metamodel = this.__getMetamodel(this.model.nodes[id]['$type']), type = this.__getType(this.model.nodes[id]['$type']), isConnector = (this.metamodels[metamodel]['connectorTypes'][type] != undefined), neighbors = []; this.model.edges.forEach( function(edge) { if( edge['src'] == id && ! _utils.contains(neighbors,edge['dest']) ) neighbors.push(edge['dest']); else if( edge['dest'] == id && ! _utils.contains(neighbors,edge['src']) ) neighbors.push(edge['src']); }); if( isConnector ) { if( (res = this.__crudOp( metamodel, ['disconnect'], neighbors, '__deleteConnector', {'id':id})) ) return res; } else { for( var i in neighbors ) if( (res = this.__crudOp( metamodel, ['delete'], [neighbors[i]], '__delete', {'id':neighbors[i]})) ) return res; this.__rmnode__(id); } }, '__deleteConnector' : function(args/*id*/) { for( var i=0; i' || _dir == '*' || _dir == "out") && (_type == '*' || self.model.nodes[edge['dest']]['$type'] == _type) && ! _utils.contains(ids,edge['dest']) ) ids.push(edge['dest']); else if( edge['dest'] == _id && (_dir == '<' || _dir == '*' || _dir == "in") && (_type == '*' || self.model.nodes[edge['src']]['$type'] == _type) && ! _utils.contains(ids,edge['src']) ) ids.push(edge['src']); } return ids; } function print(str) { _util.log(str); } function setAttr(_attr,_val,_id) { if( type != 'action' ) throw 'setAttr() can only be used within actions'; if( _id == undefined ) _id = id; if( self.model.nodes[_id] == undefined ) throw 'invalid setAttr() id :: '+_id; else if( !(_attr in self.model.nodes[_id]) || _attr.charAt(0) == '$' ) throw 'invalid setAttr() attribute :: '+_attr; self.__chattr__(_id,_attr,_val); } /* evaluate provided code without the said code having access to globals (i.e., model, journal) or to 'self' (which we use above to allow non-global functions to access globals), and catching any exceptions it may throw... escaped newlines if any are unescaped */ function safe_eval(code) { var self = undefined; try { return eval(code); } catch(err) { if( err == 'IgnoredConstraint' ) return true; return {'$err':err}; } } var res = safe_eval(code); if( res != undefined && res['$err'] != undefined ) return {'$err':type+' ('+desc+') crashed on :: '+res['$err']}; /* completed accessor */ else if( type == 'accessor' ) return res; /* failed constraint */ else if( res == false ) return {'$err':type+' ('+desc+') failed'}; }, /* run actions or constraints for specified events and specified nodes 1. get types of specified nodes (note that we do a little hack for the special case of pre-create handlers because this.model.nodes does not yet contain a node with the to-be-created node's id... thus its type is read from this.next_type) 2. identify and run applicable handlers based on events and targetTypes */ '__runEventHandlers' : function(allHandlers,events,ids,handlerType) { var types2ids = {}; for( var i in ids ) { var id = ids[i]; if( id == this.next_id ) var type = this.__getType(this.next_type); else if( this.model.nodes[id] == undefined ) continue; else var type = this.__getType(this.model.nodes[id]['$type']); if( types2ids[type] == undefined ) types2ids[type] = []; types2ids[type].push(id); } for (let i in allHandlers) { let handler = allHandlers[i]; let handled = _utils.contains(events, handler['event']) || (_utils.contains(events, "validate") && handler['event'] == ""); //handle legacy events if (!handled) { continue; } if (handler['targetType'] == '*') { let result = null; for (let j in ids) { result = this.__runDesignerCode( handler['code'], handler['event'] + ' ' + handler['name'], handlerType, ids[j]); if (result) { return result; } } if (ids.length == 0) { result = this.__runDesignerCode( handler['code'], handler['event'] + ' ' + handler['name'], handlerType); if (result) { return result; } } } else { for (let j in types2ids[handler['targetType']]) { let id = types2ids[handler['targetType']][j]; let result = this.__runDesignerCode( handler['code'], handler['event'] + ' ' + handler['name'], handlerType, id); if (result) { return result; } } } } }, /**************************** MODEL VALIDATION *****************************/ /* verifies that the current model satisfies (1) the min cardinalities set by its metamodel(s) and (2) all global eventless constraints... returns the first encountered discrepancy or nothing 1. count incoming and outgoing connections of each type for each node 2. compare the above to the min cardinalities 3. run all global eventless constraints */ 'validateModel' : function(model) { var inCounts = {}, outCounts = {}, model = (model == undefined ? this.model : model), outContainments = {}, containmentTargets = {}; if( model.nodes == undefined || model.edges == undefined || model.metamodels == undefined || model.metamodels.length == 0 ) return {'$err':'provided model is either empty or not an atompm model'}; for( var i in model.edges ) { var edge = model.edges[i], srcType = this.__getType(model.nodes[edge['src']]['$type']), destType = this.__getType(model.nodes[edge['dest']]['$type']), srcMetamodel = this.__getMetamodel(model.nodes[edge['src']]['$type']), destMetamodel = this.__getMetamodel(model.nodes[edge['dest']]['$type']); if( inCounts[edge['dest']] == undefined ) inCounts[edge['dest']] = {}; if( inCounts[edge['dest']][srcType] == undefined ) inCounts[edge['dest']][srcType] = 0; inCounts[edge['dest']][srcType]++; if( outCounts[edge['src']] == undefined ) outCounts[edge['src']] = {}; if( outCounts[edge['src']][destType] == undefined ) outCounts[edge['src']][destType] = 0; outCounts[edge['src']][destType]++; if ( outContainments[edge['src']] == undefined ) { outContainments[edge['src']] = []; } if (destType in this.metamodels[destMetamodel]['connectorTypes'] && this.metamodels[destMetamodel]['connectorTypes'][destType] == 'containment') { outContainments[edge['src']].push(edge['dest']); } if ( containmentTargets[edge['src']] == undefined ) { containmentTargets[edge['src']] = []; } if (srcType in this.metamodels[srcMetamodel]['connectorTypes'] && this.metamodels[srcMetamodel]['connectorTypes'][srcType] == 'containment') { containmentTargets[edge['src']].push(edge['dest']); } } var checked_for_loops = []; for( var id in model.nodes ) { var metamodel = this.__getMetamodel(model.nodes[id]['$type']), type = this.__getType(model.nodes[id]['$type']); for( var i in this.metamodels[metamodel]['cardinalities'][type] ) { var cardinality = this.metamodels[metamodel]['cardinalities'][type][i], tc = cardinality['type']; if( cardinality['dir'] == 'out' && cardinality['min'] > (outCounts[id] == undefined || outCounts[id][tc] == undefined ? 0 : outCounts[id][tc]) ) return {'$err':'insufficient outgoing connections of type '+tc+' for '+model.nodes[id]['$type']+'/'+id}; else if( cardinality['dir'] == 'in' && cardinality['min'] > (inCounts[id] == undefined || inCounts[id][tc] == undefined ? 0 : inCounts[id][tc]) ) return {'$err':'insufficient incoming connections of type '+tc+' for '+model.nodes[id]['$type']+'/'+id}; } if (checked_for_loops.indexOf(id) < 0 && !(type in this.metamodels[metamodel]['connectorTypes'])) { var visited = [], tv = [id]; // eslint-disable-next-line no-inner-declarations function dfs(to_visit) { var curr = to_visit.pop(); if( curr == undefined ) return undefined; // no more to check else if( visited.indexOf(curr) > -1 ) return {'$err':'containment loop found for ' + model.nodes[id]['$type']+'/'+id}; // error: loop found! else { visited.push(curr); // find all (containment) associations linked to the object, and add their targets to the to_visit list. for ( var oc_idx in outContainments[curr] ) { to_visit = to_visit.concat(containmentTargets[outContainments[curr][oc_idx]]); } return dfs( to_visit ); } } var res = dfs(tv); if (res != undefined) { return res; } checked_for_loops= checked_for_loops.concat(visited); } } for (let metamodel in this.metamodels) { let err = this.__runEventHandlers(this.metamodels[metamodel]['constraints'], ['validate'], [], 'constraint'); if (err) return err; } }, /**************************** MODEL COMPILATION ****************************/ /* compile the current model and the given CS model into an icon definition metamodel 0. the entire function body is wrapped in a try/catch... this is our lazy approach to verifying that the current model is indeed a valid model of an icon definition metamodel 1. if the current model is missing the CS formalism, return error 2. extract information about types from current model a) find all ConcreteSyntax/Icons and ConcreteSyntax/Links b) map all CS/Icons to their IconIcon in the CS model (argument) c) map all CS/Icons to the nodes they're [transitively] connected to (except their IconContents links) d) save all edges between contained nodes from step c) e) enhance every contained node (from step c)) with information about its associated IconIcon (e.g., position, orientation)... this is needed so that the final '$contents' attributes of each generated *Icon hold sufficient information to render icons as the user specified them... note that position attributes are adjusted to make them relative to the containing IconIcon's top-left corner e*) enhance nodes contained within Links with link decorator positioning information (e.g., xratio, yoffset) f) when pre-defined arrowheads/tails have been selected by the user, pretend the user has actually drawn them s.t. they get handled by link decorator positioning code during modelling... in practice: i. identify pre-defined arrowheads/tails ii. locate corresponding drawings within relevant Link's LinkIcon $contents iii. copy them into relevant Link's compiled $contents iv. enhance them with link decorator information (c.f., step e*) 3. construct mm.types based on information from step 2... the resulting mm.types wil look very much like ConcreteSyntax.types, with a few added 'special' attributes (e.g., $asuri, $contents, etc.) 4. check whether all non-abstract types have an icon, and no abstract types have an icon 5. return mm stringified (ensures no references to objects in this.model are returned) */ 'compileToIconDefinitionMetamodel' : function(csm, asmm) { var CS = '/Formalisms/__LanguageSyntax__/ConcreteSyntax/ConcreteSyntax'; try { /* 1 */ if( ! _utils.contains(this.model.metamodels,CS) ) throw 'icon definition models must have the '+CS+' formalism loaded'; else var model = _utils.jsonp(this.read()); nodes = {}; for (var id in model.nodes) { if (model.nodes[id]['$type'].slice(0, CS.length) == CS) { nodes[id] = model.nodes[id]; } } model.nodes = nodes; /* 2 */ var mm = {'types':{}, 'constraints':[], 'actions':[], 'cardinalities':{}, 'legalConnections':{}, 'connectorTypes':{}, 'types2parentTypes':{}}, iids = [], iids2contents = {}, ids2csids = {}, self = this, outNeighbors = /* returns the given node's outbound neighbors */ function(source) { return model.edges.filter(function(edge) {return edge['src'] == source && model.nodes[edge['dest']] != undefined;}). map(function(edge) {return edge['dest'];}); }, getConnectedNodes = /* compute the [transitive] contents of 'container'... this function is a bit of an oversimplification: it makes the reasonable but not necessarily correct assumption that anything that is [transitively] connected to a CS/Icon or CS/Link is inside it */ function(container,contents) { var _contents = {}; outNeighbors(container).forEach( function(n) { if( !(n in contents) ) _contents[n] = 1; }); if( _utils.keys(_contents).length == 0 ) return contents; contents = _utils.mergeDicts([contents,_contents]); return _utils.mergeDicts( _utils.keys(_contents).map( function(_c) {return getConnectedNodes(_c,contents);} )); }; /* 2a */ for( var id in model.nodes ) if( model.nodes[id]['$type'] == CS+'/Icon' || model.nodes[id]['$type'] == CS+'/Link' ) { iids.push(id); iids2contents[id] = {'nodes':{},'edges':[]}; } /* 2b */ csm = _utils.jsonp(csm); for( var csid in csm.nodes ) { var id = csm.nodes[csid]['$asuri']['value'].match(/.*\/(.*)\.instance$/)[1]; ids2csids[id] = csid; } iids.forEach( function(iid) { /* 2c */ _utils.keys(getConnectedNodes(iid,{})).filter( function(id) { return model.nodes[id]['$type'] != CS+'/IconContents'; }).forEach( function(id) { iids2contents[iid].nodes[id] = model.nodes[id]; }); /* 2d */ model.edges.forEach( function(edge) { if( iids2contents[iid].nodes[edge['src']] != undefined && iids2contents[iid].nodes[edge['dest']] != undefined ) iids2contents[iid].edges.push(edge); }); /* 2e */ var iidCSIcon = csm.nodes[ ids2csids[iid] ]; for( var vid in iids2contents[iid].nodes ) { var vidCSIcon = csm.nodes[ ids2csids[vid] ], vidContentsNode = iids2contents[iid].nodes[vid]; ['position','orientation','scale','link-style'].forEach( function(_) {vidContentsNode[_] = vidCSIcon[_];}); var vidContentsNodePosition = vidContentsNode['position']['value'], iidCSIconPosition = iidCSIcon['position']['value'], vidContentsNodeRelX = vidContentsNodePosition[0] - iidCSIconPosition[0], vidContentsNodeRelY = vidContentsNodePosition[1] - iidCSIconPosition[1]; vidContentsNode['position']['value'] = [vidContentsNodeRelX,vidContentsNodeRelY]; /* 2e* */ if( model.nodes[iid]['$type'] == CS+'/Link' ) { var sx = iidCSIcon['scale']['value'][0], sy = iidCSIcon['scale']['value'][1], linkPathBBox = {'x':sx*35, 'y':sy*77, 'width': sx*198, 'height': sy*(model.nodes[iid]['link-style']['stroke-width'] || 1)}; vidContentsNode['position']['value'] = [0,0]; vidContentsNode['$linkDecoratorInfo'] = {'type':'map', 'value': {'xratio' :(vidContentsNodeRelX-linkPathBBox.x) / (linkPathBBox.width-linkPathBBox.x), 'yoffset':vidContentsNodeRelY - (linkPathBBox.y+linkPathBBox.height/2)}}; } } /* 2f */ if( model.nodes[iid]['$type'] == CS+'/Link' ) { var contents = csm.nodes[ids2csids[iid]]['$contents']['value'].nodes, sy = iidCSIcon['scale']['value'][1]; ['arrowHead','arrowTail'].forEach( function(at) { if( !(at in model.nodes[iid]) ) throw 'migrate to new Link specification means to compile'; var a = model.nodes[iid][at]['value']; if( a != 'custom' ) for( var vid in contents ) if( 'mapper' in contents[vid] && ( _styleinfo[a + ':' + at] ) && (matches = contents[vid]['mapper']['value'].match("^'"+a+":"+at+":(.*)';")) ) { iids2contents[iid].nodes[vid] = contents[vid]; iids2contents[iid].nodes[vid]['mapper']['value'] = ''; iids2contents[iid].nodes[vid]['position']['value'] = [0,0]; iids2contents[iid].nodes[vid]['$linkDecoratorInfo'] = {'type':'map', 'value': {'xratio' :(at == 'arrowHead' ? -1 : 1), 'yoffset':-_styleinfo[a + ':' + at]/2*sy}}; break; } }); } /* 3 */ var node = model.nodes[iid]; type = node['typename']['value']; isConnectorType = 'link-style' in node; mm.types[type] = []; self.metamodels[CS]. types[(isConnectorType ? 'Link' : 'Icon')].forEach( function(attr) { if( _utils.contains(['link-style','typename','mapper','parser','position'],attr['name']) ) mm.types[type].push( {'name': attr['name'], 'type': node[attr['name']]['type'], 'default': node[attr['name']]['value']}); else mm.types[type].push(attr); }); mm.types[type].push( {'name': '$contents', 'type': 'map', 'default': iids2contents[iid]}, {'name': '$asuri', 'type': 'string', 'default': '-1'}); if( isConnectorType ) mm.types[type].push( {'name': '$segments', 'type': 'map>', 'default': {}}); mm.cardinalities[type] = []; mm.types2parentTypes[type] = []; }); /* 4 */ var types = [], abstractTypes = []; for (var idx in asmm["constraints"]) { var curr_constraint = asmm["constraints"][idx]; if (curr_constraint["name"] == "noAbstractInstances") { abstractTypes.push(curr_constraint["targetType"]); } } for (var curr_type in asmm["types"]) { if ((curr_type + 'Link' in mm["types"]) || (curr_type + 'Icon' in mm["types"])) { if (abstractTypes.indexOf(curr_type) >= 0) { return {'$err':'abstract type '+curr_type+' cannot have a visual representation'}; } } else { if (abstractTypes.indexOf(curr_type) < 0) { return {'$err':'concrete type '+curr_type+' needs to have a visual representation'}; } } } for (var curr_type in mm["types"]) { if (!(curr_type.slice(0, -4) in asmm["types"])) { return {'$err':'type '+curr_type.slice(0, -4)+' not found in the abstract syntax metamodel, visual representation ' + curr_type + ' invalid'}; } } /* 5 */ return _utils.jsons(mm,null,"\t"); } catch(err) { return {'$err':'invalid metamodel model, crashed on :: '+err}; } }, /* compile the current model into a metamodel 0. the entire function body is wrapped in a try/catch... this is our lazy approach to verifying that the current model is indeed a valid model of a metamodel 1. if the current model is not an ER or a SCD model, return error 2. if the current model is a SCD model, transform it into an ER model before beginning compilation (via _mt.transform) 3. copy information about types, constraints, actions, cardinalities, connectorTypes and types2parentTypes from current model to mm 4. add any missing cardinalities (relationships between entities define legal connections but the user might have omitted to specify their cardinalities), then construct legalConnections and store it in mm 5. return mm stringified (ensures no references to objects in this.model are returned) */ 'compileToMetamodel' : function() { var ER = '/Formalisms/__LanguageSyntax__/EntityRelationship/EntityRelationship', SCD = '/Formalisms/__LanguageSyntax__/SimpleClassDiagram/SimpleClassDiagram', isolateMMModel = /* remove all non-ER/SCD entities from the provided model... doing so considerably eases compilation */ function(m) { m = _utils.jsonp(m); for( var id in m.nodes ) if( ! m.nodes[id]['$type'].match('^'+ER) && ! m.nodes[id]['$type'].match('^'+SCD) ) delete m.nodes[id]; var keepEdges = []; m.edges.forEach( function(edge,i) { if( edge['src'] in m.nodes && edge['dest'] in m.nodes ) keepEdges.push(edge); }); m.edges = keepEdges; return m; }; try { /* 1-2 */ if( _utils.contains(this.model.metamodels,ER) && _utils.contains(this.model.metamodels,SCD) ) throw 'metamodel models should not have more than one loaded metametamodel'; else if( _utils.contains(this.model.metamodels,ER) ) var model = isolateMMModel(this.read()); else if( _utils.contains(this.model.metamodels,SCD) ) var model = _mt.transform( _utils.jsons(isolateMMModel(this.read())), 'SimpleClassDiagram-2-EntityRelationship'); else throw 'metamodel models should have at least one loaded metametamodel'; /* 3 */ var mm = {'types':{}, 'constraints':[], 'actions':[], 'cardinalities':{}, 'legalConnections':{}, 'connectorTypes':{}, 'types2parentTypes':{}}; for( var id in model.nodes ) { var node = model.nodes[id]; if( node['$type'] == ER+'/Entity' || node['$type'] == ER+'/Relationship' ) { var type = node['name']['value']; mm.types[type] = []; node['attributes']['value'].forEach( function(attr) {mm.types[type].push(attr);}); node['constraints']['value'].forEach( function(constraint) { constraint['targetType'] = type; mm.constraints.push(constraint); }); node['actions']['value'].forEach( function(action) { action['targetType'] = type; mm.actions.push(action); }); mm.cardinalities[type] = node['cardinalities']['value']; if( node['linktype'] != undefined ) mm.connectorTypes[type] = node['linktype']['value']; } else if( node['$type'] == ER+'/GlobalConstraint' ) mm.constraints.push( {'name':node['name']['value'], 'event':node['event']['value'], 'targetType':'*', 'code':node['code']['value']}); else if( node['$type'] == ER+'/GlobalAction' ) mm.actions.push( {'name':node['name']['value'], 'event':node['event']['value'], 'targetType':'*', 'code':node['code']['value']}); else throw 'node "'+id+'" does not conform to the '+ER+' metamodel'; } mm.types2parentTypes = model.types2parentTypes || {}; /* 4 */ var types2legalNeighborTypes = {}, addMissingCardinalities = function(t1,t2,dir) { /* if there is no cardinality between t1 and t2 for dir, add a default cardinality... 1:1 for links 0:Infinity for nodes */ if( ! mm.cardinalities[t1].some( function(c) {return c['type'] == t2 && c['dir'] == dir;} ) ) { if( mm.connectorTypes[t1] ) mm.cardinalities[t1].push( {'dir':dir, 'type':t2, 'min':'0', 'max':'1'}); else mm.cardinalities[t1].push( {'dir':dir, 'type':t2, 'min':'0', 'max':'Infinity'}); } }; model.edges.forEach( function(edge) { var srcType = model.nodes[edge['src']]['name']['value'], destType = model.nodes[edge['dest']]['name']['value']; addMissingCardinalities(srcType,destType,'out',mm.connectorTypes[srcType]); addMissingCardinalities(destType,srcType,'in',mm.connectorTypes[destType]); }); for( var type in mm.types ) { if( types2legalNeighborTypes[type] == undefined ) types2legalNeighborTypes[type] = []; mm.cardinalities[type].forEach( function(cardinality) { if( cardinality['dir'] == 'out' ) types2legalNeighborTypes[type].push(cardinality['type']); }); } for( var type in types2legalNeighborTypes ) { if( mm.connectorTypes[type] != undefined ) continue; types2legalNeighborTypes[type].forEach( function(ntype) { if (types2legalNeighborTypes[ntype] == undefined){ let msg = "Error! Problem with edges for class: " + type +"\nFound constraints: " + JSON.stringify(types2legalNeighborTypes[type]); throw msg; } types2legalNeighborTypes[ntype].forEach( function(nntype) { if( mm.legalConnections[type] == undefined ) mm.legalConnections[type] = {}; if( mm.legalConnections[type][nntype] == undefined ) mm.legalConnections[type][nntype] = []; mm.legalConnections[type][nntype].push(ntype); }); }); } /* 5 */ return _utils.jsons(mm,null,"\t"); } catch(err) { return {'$err':'invalid metamodel model, crashed on :: ' + err}; } }, /************************* JOURNALING + UNDO/REDO **************************/ 'journal':[], 'journalIndex':0, /* NOTE: on this.undoredoJournal this.undoredoJournal contains cud operations performed during the last undo()/redo() call provided no user-operations was performed since the said call (in which case this.undoredoJournal is empty)... undo/redo ops need to be logged for __changelog() to be able to return their effects... however, they should not be logged in the main journal since all they conceptually do is move a cursor in it... in practice, this.undoredoJournal is emptied on every call to undo(), redo() and __setStepCheckpoint() */ /* create a checkpoint : add an entry in the log used as a delimiter to know where to stop when restoring (i.e., undoing failed pre-/post-actions or crud ops) */ '__checkpoint' : function() { this.__log({'op':'MKCHKPT'}); }, /* deletes the last checkpoint of the current model (other than tidying the journal, there's no reason for ever clearing unused checkpoints) */ '__clearCheckpoint' : function() { for( var i=this.journal.length-1; i>=0; i-- ) if( this.journal[i]['op'] == 'MKCHKPT' ) { this.journal.splice(i,1); this.journalIndex--; break; } }, /* case 1: 'this.undoredoJournal is defined (possibly empty)' returns the operations performed by the last undo()/redo() case 2: 'this.undoredoJournal = undefined' returns a copy of the portion of the journal that describes the changes made by the last user-operation... note that user-operations always call __setStepCheckpoint before running */ '__changelog' : function() { if( this.undoredoJournal != undefined ) return _utils.clone( this.undoredoJournal.splice(0) ); var ji = this.journalIndex; while( ji > 0 ) if( this.journal[--ji]['op'] == 'MKSTPCHKPT' ) break; return _utils.clone( this.journal.slice(ji+1,this.journalIndex) ); }, /* case 1: 'log=undefined' logs an internal cud operation into the journal... if the current index in the journal is anything but the end of the journal, clear everything after the index (this effectively erases the command "future-history" when editing an "undone" model) case 2: 'log="UNDOREDO"' logs an internal cud operation into this.undoredoJournal case 3: 'log="DONTLOG"' do nothing legal logging commands: MKNODE id,node RMNODE id,node MKEDGE id,id RMEDGE id,id CHATTR id,attr,new_val,old_val LOADMM name,mm DUMPMM name,mm RESETM name,model MKCHKPT MKSTPCHKPT MKUSRCHKPT */ '__log' : function(step,log) { if( log == undefined ) { if( this.journalIndex != this.journal.length ) this.journal.splice(this.journalIndex); this.journal.push(step); this.journalIndex++; } else if( log == 'UNDOREDO' ) this.undoredoJournal.push(step); //else if( log == 'DONTLOG' ) // ; }, /* redo a single step 1. identify the nature of the logged operation 2. reproduce its effects (these are logged in this.undoredoJournal) */ '__redo' : function(step) { var log = 'UNDOREDO'; if( step['op'] == 'CHATTR' ) this.__chattr__(step['id'],step['attr'],step['new_val'],log); else if( step['op'] == 'DUMPMM' ) this.__dumpmm__(step['name'],log); else if( step['op'] == 'LOADMM' ) this.__loadmm__(step['name'],step['mm'],log); else if( step['op'] == 'MKEDGE' ) this.__mkedge__(step['id1'],step['id2'],step['i'],log); else if( step['op'] == 'MKNODE' ) this.__mknode__(step['id'],_utils.jsonp(step['node']),log); else if( step['op'] == 'RESETM' ) this.__resetm__(step['new_name'],step['new_model'],false,log); else if( step['op'] == 'RMEDGE' ) this.__rmedge__(step['i'],log); else if( step['op'] == 'RMNODE' ) this.__rmnode__(step['id'],log); }, /* redo all of the changes until the next step-checkpoint or until after the specified user-checkpoint, if any... when complete the journal index is after the redone MKSTPCHKPT/MKUSRCHKPT entry... redoing when the journal index is at the end of the journal will have no effect */ 'redo' : function(uchkpt) { this.undoredoJournal = []; var stopMarkerReached = (uchkpt == undefined ? function(step) {return step['op'] == 'MKSTPCHKPT';} : function(step) {return uchkptEncountered && step['op'] == 'MKUSRCHKPT';}), self = this, uchkptEncountered = false, uchkptReached = function(step) {return step['op'] == 'MKUSRCHKPT' && step['name'] == uchkpt;}, uchkptFound = function(i) { while( i < self.journal.length ) if( uchkptReached(self.journal[i++]) ) return true; return false; }; if( uchkpt == undefined || uchkptFound(this.journalIndex) ) while( this.journalIndex < this.journal.length ) { if( uchkpt != undefined && ! uchkptEncountered && uchkptReached(this.journal[this.journalIndex]) ) uchkptEncountered = true; if( this.journal[++this.journalIndex] == undefined || stopMarkerReached( this.journal[this.journalIndex] ) ) break; else this.__redo(this.journal[this.journalIndex]); } return {'changelog':this.__changelog()}; }, /* undo every logged operation until a MKCHKPT is reached (and remove them and the said MKCHKPT from the journal)... note that this operation is only called internally and that the journalIndex will always be at the end of the journal when it's called (and after its called) */ '__restoreCheckpoint' : function() { while( this.journal.length > 0 ) { var step = this.journal.pop(); if(step['op'] == 'MKCHKPT' ) break; else this.__undo(step,'DONTLOG'); } this.journalIndex = this.journal.length; }, /* create a step-checkpoint : add an entry in the log used as a delimiter to know where to stop when undoing/redoing (i.e., on client undo/redo) 1. create new step-checkpoint or re-use a 'zombie' step-checkpoint (zombie step-checkpoints (SC) are SCs associated to failed or effectless user operations... they are recognizable as SCs with no following log entries... there's at most 1 zombie SC in the log at any given time) */ '__setStepCheckpoint' : function() { this.undoredoJournal = undefined; if( this.journal.length == 0 || this.journal[this.journal.length-1]['op'] != 'MKSTPCHKPT' ) this.__log({'op':'MKSTPCHKPT'}); }, /* create a user-checkpoint : add an entry in the log used as a delimiter to enable undoing/redoing until a specified marker 1. create new step-checkpoint or re-use a 'zombie' user-checkpoint (zombie user-checkpoints (UC) are UCs associated to failed or effectless user operations... they are recognizable as same-name UCs with no following log entries... there's at most 1 zombie UC per name in the log at any given time) */ 'setUserCheckpoint' : function(name) { this.undoredoJournal = undefined; if( this.journal.length == 0 || this.journal[this.journal.length-1]['op'] != 'MKUSRCHKPT' || this.journal[this.journal.length-1]['name'] != name ) this.__log({'op':'MKUSRCHKPT','name':name}); }, /* undo a single step 1. identify the nature of the logged operation 2. invert its effects (these may be ignored (log = 'DONTLOG') or logged in this.undoredoJournal (log = 'UNDOREDO') */ '__undo' : function(step,log) { if( step['op'] == 'CHATTR' ) this.__chattr__(step['id'],step['attr'],step['old_val'],log); else if( step['op'] == 'DUMPMM' ) this.__loadmm__(step['name'],step['mm'],log); else if( step['op'] == 'LOADMM' ) this.__dumpmm__(step['name'],log); else if( step['op'] == 'MKEDGE' ) this.__rmedge__(step['i'],log); else if( step['op'] == 'MKNODE' ) this.__rmnode__(step['id'],log); else if( step['op'] == 'RESETM' ) this.__resetm__(step['old_name'],step['old_model'],false,log); else if( step['op'] == 'RMEDGE' ) this.__mkedge__(step['id1'],step['id2'],step['i'],log); else if( step['op'] == 'RMNODE' ) this.__mknode__(step['id'],_utils.jsonp(step['node']),log); }, /* undo all of the changes since the last step-checkpoint or since the specified user-checkpoint, if any... when complete the journal index is on the undone MKSTPCHKPT/MKUSRCHKPT entry... undoing when the journal index is 0 or when a non-existing user-checkpoint is given will have no effect */ 'undo': function(uchkpt) { this.undoredoJournal = []; var stopMarkerReached = (uchkpt == undefined ? function(step) {return step['op'] == 'MKSTPCHKPT';} : function(step) {return step['op'] == 'MKUSRCHKPT' && step['name'] == uchkpt;}), self = this, stopMarkerFound = function(i) { while( --i >= 0 ) if( stopMarkerReached(self.journal[i]) ) return true; return false; }; if( uchkpt == undefined || stopMarkerFound(this.journalIndex) ) while( this.journalIndex > 0 ) if( stopMarkerReached( this.journal[--this.journalIndex] ) ) break; else this.__undo(this.journal[this.journalIndex],'UNDOREDO'); return {'changelog':this.__changelog()}; }, /****************************** INTERNAL CUD *******************************/ /* the following functions are super basic and low-level, they offer cud (no read) commands on this' internal data structures... their main purposes are (1) to localize the said cud operations, and (2) to log everything they do... logging enables undoing and redoing (on constraint/action/... failure or on client requests) and facilitates change pushing (i.e., push a short change log rather the full model)... note that it is assumed that only valid parameters are passed to these functions... last but not least, the optional 'log' parameter is used when undoing/redoing to log undoing/ redoing cud ops elsewhere than in this.journal __chattr__ change an attribute's value > log id,attr,new_val,old_val __dumpmm__ remove mm from this.model.metamodels and this.metamodels > log name,mm __loadmm__ add a mm to this.model.metamodels and this.metamodels > log name,mm __mkedge__ add an edge to this.model.edges... optional 'i' parameter specifies index of new edge in this.model.edges > log id1,id2,i __mknode__ add a node to this.model.nodes > log id,node __resetm__ when the 'insert' parameter is false, replaces the current model with another + updates this.next_id to account for ids in loaded model + updates model.metamodels to account for metamodels loaded before the model when the 'insert' parameter is true, inserts the given model alongside the current model + alters the given model's ids to avoid clashes with existing ids + updates this.next_id... the logged value of 'insert' ends up being the offset we applied to the provided model's ids > log new_name,new_model,old_name,old_model,insert __rmedge__ remove an edge from this.model.edges > log id1,id2,i __rmnode__ remove a node from this.model.nodes > log id,node note: these functions never log any 'live' data into the log (i.e., any references that could be altered elsewhere thereby altering the journal's contents) */ '__chattr__' : function(id,attr,new_val,log) { var getattr = undefined, setattr = undefined, attrval = function(v) {return (v == undefined ? v : _utils.jsonp(v));}, self = this; if( attr.match(/.+\/.+/) ) { var curr = this.model.nodes[id]; for( var i in (path = attr.split('/')) ) curr = curr[path[i]]; getattr = function() {return curr['value'];}; setattr = function(v) {curr['value'] = v;}; } else { getattr = function() {return self.model.nodes[id][attr]['value'];}; setattr = function(v) {self.model.nodes[id][attr]['value'] = v;}; } var _old_val = _utils.jsons(getattr()), _new_val = _utils.jsons(new_val); if( _old_val == _new_val ) return; setattr( attrval(_new_val) ); this.__log( {'op':'CHATTR', 'id':id, 'attr':attr, 'new_val': attrval(_new_val), 'old_val': attrval(_old_val)}, log); }, '__dumpmm__' : function(name,log) { for( var i in this.model.metamodels ) if( this.model.metamodels[i] == name ) { this.model.metamodels.splice(i,1); break; } var mm = this.metamodels[name]; delete this.metamodels[name]; this.__log( {'op':'DUMPMM', 'name':name, 'mm':_utils.jsons(mm)}, log); }, '__loadmm__' : function(name,mm,log) { this.metamodels[name] = eval('('+ mm +')'); if( ! _utils.contains(this.model.metamodels,name) ) this.model.metamodels.push(name); this.__log( {'op':'LOADMM', 'name':name, 'mm':mm}, log); }, '__mkedge__' : function(id1,id2,i,log) { if( i == undefined ) i = this.model.edges.push({'src':id1, 'dest':id2})-1; else this.model.edges.splice(i,0,{'src':id1, 'dest':id2}); this.__log( {'op':'MKEDGE', 'id1':id1, 'id2':id2, 'i':i}, log); }, '__mknode__' : function(id,node,log) { this.model.nodes[id] = node; this.__log( {'op':'MKNODE', 'id':id, 'node':_utils.jsons(node)}, log); }, '__resetm__' : function(new_name,new_model,insert,log) { var old_model = this.read(), old_name = this.name; if( insert ) { var _new_model = eval('('+ new_model +')'); for( var id in _new_model.nodes ) this.model.nodes[parseInt(id)+this.next_id] = _new_model.nodes[id]; _new_model.edges.forEach( function(edge) { this.model.edges.push( {'src': parseInt(edge.src)+this.next_id, 'dest': parseInt(edge.dest)+this.next_id}); }, this); new_model = this.read(); insert = this.next_id; } else { this.model = eval('('+ new_model +')'); for( var mm in this.metamodels ) if( ! _utils.contains(this.model.metamodels,mm) ) this.model.metamodels.push(mm); } this.name = new_name; for( var id in this.model.nodes ) if( id >= this.next_id ) this.next_id = parseInt(id)+1; this.__log( {'op':'RESETM', 'new_name': new_name, 'new_model':new_model, 'old_name': old_name, 'old_model':old_model, 'insert':insert}, log); }, '__rmedge__' : function(i,log) { var edge = this.model.edges.splice(i,1).pop(); this.__log( {'op':'RMEDGE', 'i':i, 'id1':edge['src'], 'id2':edge['dest']}, log); }, '__rmnode__' : function(id,log) { node = this.model.nodes[id]; delete this.model.nodes[id]; this.__log( {'op':'RMNODE', 'id':id, 'node':_utils.jsons(node)}, log); }, /***************************** INTERNAL UTILS ******************************/ /* splits a full type of the form '/path/to/metamodel/type' and returns '/path/to/metamodel' */ '__getMetamodel' : function(fulltype) { return fulltype.match(/(.*)\/.*/)[1]; }, /* splits a full type of the form '/path/to/metamodel/type' and returns 'type' */ '__getType' : function(fulltype) { return fulltype.match(/.*\/(.*)/)[1]; } };