/*******************************************************************************
AToMPM - A Tool for Multi-Paradigm Modelling
Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca)
This file is part of AToMPM.
AToMPM is free software: you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
AToMPM is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with AToMPM. If not, see .
*******************************************************************************/
/* 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... */
{
/********************************* 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') or both
(_dir = '*') neighbor ids for specified type (if any)
print(str)
print something to the console that launched the server
setAttr(_attr,_val[,_id])
((this function is only available in actions))... update the
requested attr of the specified node using __chattr__ (s.t.
the change is logged)...
TBA:: type-checking on _val
basic checks are made on input parameters to aid in debugging faulty
actions and constraints... for functions with id parameters, if no
id is given, we use the id passed to __runDesignerCode... which is
either the id of the node that "owns" the current constraint|action|
accessor, or undefined if the parameter was omitted */
var self = this;
function getAttr(_attr,_id)
{
if( _id == undefined )
_id = id;
if( self.model.nodes[_id] == undefined )
throw 'invalid getAttr() id :: '+_id;
else if( !(_attr in self.model.nodes[_id]) )
throw 'invalid getAttr() attribute :: '+_attr;
if( _attr.charAt(0) == '$' )
return self.model.nodes[_id][_attr];
else if( typeof self.model.nodes[_id][_attr]['value'] == 'object' )
return _utils.clone(self.model.nodes[_id][_attr]['value']);
else
return self.model.nodes[_id][_attr]['value'];
}
function getAttrNames(_id)
{
if( _id == undefined )
_id = id;
if( self.model.nodes[_id] == undefined )
throw 'invalid getAttrNames() id :: '+_id;
return Object.getOwnPropertyNames(self.model.nodes[_id]);
}
function hasAttr(_attr,_id)
{
if( _id == undefined )
_id = id;
if( self.model.nodes[_id] == undefined )
throw 'invalid getAttr() id :: '+_id;
return _attr in self.model.nodes[_id];
}
function getAllNodes(_fulltypes)
{
if( _fulltypes != undefined && !(_fulltypes instanceof Array) )
throw 'invalid getAllNodes() types array :: '+_fulltypes;
var ids = [];
for( var _id in self.model.nodes )
{
if( _fulltypes == undefined ||
_utils.contains(_fulltypes,self.model.nodes[_id]['$type']) )
ids.push(_id);
}
return ids;
}
function getNeighbors(_dir,_type,_id)
{
if( _id == undefined )
_id = id;
if( _type == undefined )
_type = '*';
if( self.model.nodes[_id] == undefined )
throw 'invalid getNeighbors() id :: '+_id;
var ids = [];
for( var i in self.model.edges )
{
var edge = self.model.edges[i];
if( edge['src'] == _id &&
(_dir == '>' || _dir == '*') &&
(_type == '*' || self.model.nodes[edge['dest']]['$type'] == _type) &&
! _utils.contains(ids,edge['dest']) )
ids.push(edge['dest']);
else if( edge['dest'] == _id &&
(_dir == '<' || _dir == '*') &&
(_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( var i in allHandlers )
{
var handler = allHandlers[i];
if( _utils.contains(events,handler['event']) )
{
if( handler['targetType'] == '*' )
{
for( var j in ids )
if( (res = this.__runDesignerCode(
handler['code'],
handler['event']+' '+handler['name'],
handlerType,
ids[j])) )
return res;
if( ids.length == 0 )
if( (res = this.__runDesignerCode(
handler['code'],
handler['event']+' '+handler['name'],
handlerType)) )
return res;
}
else
for( var j in types2ids[handler['targetType']] )
{
var id = types2ids[handler['targetType']][j];
if( (res = this.__runDesignerCode(
handler['code'],
handler['event']+' '+handler['name'],
handlerType,
id)) )
return res;
}
}
}
},
/**************************** 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);
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']);
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]++;
}
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};
}
}
for( var metamodel in this.metamodels )
if( (err=this.__runEventHandlers(this.metamodels[metamodel]['constraints'], [''], [], 'constraint')) )
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. 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) {
console
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] = [];
});
/* check whether all non-abstract types have an icon, and no abstract types have an icon */
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'};
}
}
}
/* 4 */
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)
{
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];
}
}