var _ = require("lodash"),
util = require('util'),
CronJob = require('cron').CronJob,
fs = require("fs"),
url = require("url"),
md5 = require('MD5');
(c) 2013-2014 mPlane Consortium (http://www.ict-mplane.eu) Author: Fabrizio Invernizzi fabrizio.invernizzi@telecomitalia.it
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
THIS SOFTWARE IS PROVIDED BY
This is the mPlane nodejs library. The architecture and software structure is freely inspired by mPlane reference implementation written in python by Brian Trammell brian@trammell.ch.
var _ = require("lodash"),
util = require('util'),
CronJob = require('cron').CronJob,
fs = require("fs"),
url = require("url"),
md5 = require('MD5');
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
Duration regular expression
var _dur_re = /((\d+)d)?((\d+)h)?((\d+)m)?((\d+)s)?/;
var _dur_seclabel = {
'd' : 86400,
'h' : 3600,
'm' : 60,
's' : 1
};
var ELEMENT_SEP = ".";
var RANGE_SEP = " ... ";
var DURATION_SEP = " + ";
var PERIOD_SEP = " / ";
var SET_SEP = ",";
var KIND_CAPABILITY = "capability"
var KIND_SPECIFICATION = "specification"
var KIND_RESULT = "result"
var KIND_RECEIPT = "receipt"
var KIND_REDEMPTION = "redemption"
var KIND_INDIRECTION = "indirection"
var KIND_WITHDRAWAL = "withdrawal"
var KIND_INTERRUPT = "interrupt"
var KIND_EXCEPTION = "exception"
var _dow_label = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'];
var __DEFAULT_REGISTRY_FILE = "./registry.json";
////////////////////////////////////////////////////////////////////////////////////////////
var time = function(){
}
String rapresentation of past, now, future
time.TIME_PAST = "past";
time.TIME_NOW = "now";
time.TIME_FUTURE = "future"
time.TIME_INVALID = "invalid";
time.time_past = function(strVal){
return (strVal === time.TIME_PAST);
}
time.time_now = function(strVal){
return (strVal === time.TIME_NOW);
}
time.time_future = function(strVal){
return (strVal === time.TIME_FUTURE);
}
Given a valid datetime string or PAST/FUTURE/NOW string return a new Date obj or PAST/FUTURE/NOW
time.parse_time = function(valStr){
if (!valStr)
return null;
switch(valStr){
case time.TIME_PAST:
return time.TIME_PAST;
break;
case time.TIME_FUTURE:
return time.TIME_FUTURE;
break;
case time.TIME_NOW:
return time.TIME_NOW;
break;
default:
try{
return new Date(valStr);
}catch(e){
return time.TIME_INVALID;
}
}
}
Given a valid datetime or PAST/FUTURE/NOW string return a string representation Static function
time.unparse_time = function(valts){
if (!valts)
return time.TIME_INVALID;
if (valts instanceof Date)
return valts.toString();
switch(valStr){
case time.TIME_PAST:
return time.TIME_PAST;
break;
case time.TIME_FUTURE:
return time.TIME_FUTURE;
break;
case time.TIME_NOW:
return time.TIME_NOW;
break;
default:
return time.TIME_INVALID;
}
}
time.parse_dur = function(valstr){
if (!valstr)
return null
var valsec = 0;
m = _dur_re.exec(valstr);
var durSecLabels = _.keys(_dur_seclabel);
The returned obj has the input in position 0
for (var i=1 ; i<5; i++){
if (!isNaN(m[2*i])){
valsec += (parseInt(_dur_seclabel[durSecLabels[i-1]]) * parseInt(m[2*i]));
}
}
return valsec;
}
time.unparse_dur = function(valtd){
var valstr = "";
var durSecLabels = _.keys(_dur_seclabel);
The returned obj has the input in position 0
for (var i=1 ; i<5; i++){
if (valtd >= _dur_seclabel[durSecLabels[i-1]]){
valunit = parseInt(valtd / _dur_seclabel[durSecLabels[i-1]]);
valstr += valunit.toString() + durSecLabels[i-1];
valtd -= valunit * _dur_seclabel[durSecLabels[i-1]]
}
}
if (!valstr.length)
valstr = "0s";
return valstr;
}
time.unParseDur = time.parse_dur;
This is not defined in Javascript
time.total_seconds = function(valInt){
return valInt;
}
time.areEqual = function(date_A , date_B){
Are special case string?
if ((_.isString(date_A)) && ((date_A == time.TIME_FUTURE) || (date_A == time.TIME_NOW) || (date_A == time.TIME_PAST)) ){
return (date_A === date_B);
}
var dA , dB;
We convert all to Date
if (!_.isDate(date_A))
dA = new Date(date_A);
else
dA = date_A;
if (!_.isDate(date_B))
dB = new Date(date_B);
else
dB = date_B;
return (dB.getTime() === dA.getTime());
}
////////////////////////////////////////////////////////////////////////////////////////////
Defines the temporal scopes for capabilities, results, or single measurement specifications. a and b could be strings (past,now,future) or Date obj
var when = function(valstr , a , b , d , p){
this.setDate("_a" , a || null);
this.setDate("_b" , b || null);
this._d = d || null; // duration
this._p = p || null; // period
if (valstr)
this.parse(valstr)
}
Set one of the to date in when depending on the type of value we pass
when.prototype.setDate = function(whichDate , value){
if (!value){
this[whichDate] = null;
return;
}
if (value && (value === time.TIME_PAST || value === time.TIME_NOW || value === time.TIME_FUTURE))
this[whichDate] = value ;
else
(_.isDate(value))?this[whichDate]=value:this[whichDate] = new Date(value);
}
when.prototype.parse = function(valstr){
First separate the period from the value and parse it
var valsplit = valstr.split(PERIOD_SEP);
if (valsplit.length > 1){
this._p = time.parse_dur(valsplit[1]);
}else
this.p = null;
then try to split duration or range
valsplit = valsplit[0].split(DURATION_SEP); //+
if (valsplit.length > 1){
this._d = time.parse_dur(valsplit[1]);
this.setDate("_a" , valsplit[0]);
this._b = null;
}else{
this._d = null
valsplit = valsplit[0].split(RANGE_SEP);
if (valsplit.length > 1){
this.setDate("_a" , time.parse_time(valsplit[0]));
this.setDate("_b" , time.parse_time(valsplit[1]));
}
else{
this.setDate("_a" , time.parse_time(valsplit[0]));
this._a = time.parse_time(valsplit[0])
this._b = null;
}
}
}
when.prototype.is_immediate = function(){
return this._a === time.TIME_NOW;
}
when.prototype.is_forever = function(){
return this._a === time.TIME_FUTURE;
}
when.prototype.is_past = function(){
return ((this._a === time.TIME_PAST) && (this._b === time.TIME_NOW));
}
when.prototype.is_future = function(){
return ((this._a === time.TIME_NOW) && (this._b === time.TIME_FUTURE));
}
when.prototype.is_infinite = function(){
return ((this._a === time.TIME_PAST) && (this._b === time.TIME_FUTURE));
}
Return True if this scope defines a definite time or a definite time interval.
when.prototype.is_definite = function(){
if (!this._b)
return _.isNumber(this._d);
else
return ((this._a instanceof Date) && (this._b instanceof Date));
}
Return True if this temporal scope refers to a singleton measurement. Used in scheduling an enclosing Specification; has no meaning for Capabilities or Results.
when.prototype.is_singleton = function(){
return (this._a && !this._b && !this._d);
}
when.prototype._datetimes = function(tzero){
var start=null,end=null;
if (!tzero)
tzero = new Date();
else
(_.isDate(tzero))?tzero=tzero:tzero = new Date(tzero);
switch(this._a){
case time.TIME_NOW:
start = tzero;
break;
case time.TIME_PAST:
start = null;
break;
default:
start = this._a;
}
switch (this._b){
case (time.TIME_FUTURE):
end = null;
break;
case (null):
if (this._d){
end = new Date((start.getTime() + this._d*1000));//_d is in seonds, getTime in millisec
}
else
end = start;
break;
default:
end = this._b;
}
return { "start":start ,
"end" : end};
}
when.prototype.duration = function(tzero){
if (this._d)
return this._d;
if (this._b === time.TIME_FUTURE)
return null;
if (!this._b)
return 0;
else{
var se = this._datetimes(tzero);
return (se[1].getTime() - se[0].getTime());
return (se.end.getTime() - se.start.getTime());
}
}
Returns a tuple with delays for timers to signal the start and end of a temporal scope, given a specified time zero, which defaults to the current system time.
The start delay is defined to be zero if the scheduled start time has already passed or the temporal scope is immediate (i.e., starts now). The start delay is None if the temporal scope has expired (that is, the current time is after the calculated end time)
The end delay is defined to be None if the temporal scope has already expired, or if the temporal scope has no scheduled end (is infinite or a singleton). End delays are calculated to give priority to duration when a temporal scope is expressed in terms of duration, and to prioritize end time otherwise.
Used in scheduling an enclosing Specification for execution. Has no meaning for Capabilities or Results.
when.prototype.timer_delays = function(tzero){
var se=null,sd=null,ed=null;
if (!tzero)
tzero = new Date();
var se = this._datetimes(tzero);
sd = time.total_seconds(se.start - tzero);
if (sd < 0)
sd = 0;
if (this._b && (!time.time_future(this._b))){
ed = time.total_seconds(se.end - tzero);
}else{
if (this._d){
ed = (sd + time.total_seconds(this._d)*1000); // everything in millisec
}
}
detect expired temporal scope
if (ed && ed<0){
sd = null;
ed = null;
}
return {
sd: sd/1000,
ed: ed/1000
};
}
when.prototype.period = function(){
return this._p;
}
when.prototype.in_scope = function(t, tzero){
return ((this.sort_scope(t, tzero) == 0));
}
Return < 0 if time t falls before this scope, 0 if time t falls within the scope, or > 0 if time t falls after this scope.
when.prototype.sort_scope = function(t, tzero){
if (t === time.TIME_NOW){
if ((this._a === time.TIME_NOW) || (this._b === time.TIME_NOW))
return 0;
else{
if (!tzero)
tzero = new Date();
t = tzero;
}
}
var se = this._datetimes(tzero);
if (se.start && (t < se.start))
return time.total_seconds(t - se.start);
else{
if (se.end && (t > se.end))
return time.total_seconds(t - se.end);
else
return 0;
}
}
when.prototype.follows = function(s,tzero){
if ((s.in_scope(this._a, tzero)) || (s.in_scope(this._b, tzero)))
return true;
return false
}
var when_infinite = new when(null , time.TIME_PAST, time.TIME_FUTURE);
////////////////////////////////////////////////////////////////////////////////////////////
Defines a schedule for repeated operations based on crontab-like sets of months, days, days of weeks, hours, minutes, and seconds. Used to specify repetitions of single measurements in a Specification. It is a wrap-aroud of node cron library (https://github.com/ncb000gt/node-cron)
var Scheduler = function(){
this.jobs = []; // Array of all scheduled jobs
}
Schedules a new job —- params cronTime: [REQUIRED] - The time to fire off your job. This can be in the form of cron syntax or a JS Date object. onTick: [REQUIRED] - The function to fire at the specified time. onComplete: [OPTIONAL] - A function that will fire when the job is complete, when it is stopped.
Scheduler.prototype.addJob = function(cronTime, onTick, onComplete){
var job = new CronJob(cronTime, onTick, onComplete,true);
this.jobs.push(job);
}
////////////////////////////////////////////////////////////////////////////////////////////
Represents a primitive mPlane data type. Primitive types define textual and native representations for data elements, and convert between the two.
In general, client code will not need to interact with Primitives; conversion between strings and values is handled automatically by the Statement and Notification classes. Here we have a single class with a type and single functions that behave differently for dirent primitives (More procedural, less object oriented) ////////////////////////////////////////////////////////////////////////////////////////////
constructor
var Primitive = function(type){
this.setType(type);
}
The primitive type is the key of each type If some specific constrains function os defined, it overloads the default one
var PRIMITIVES = {
"UNDEF":{
label: "undef",
isValid: function(value){return true;},
unParse: function(value){ return value; },
parse: function(value){return value}
},
"STRING":{
label: "string",
isValid: function(value){ return _.isString(value); },
unParse: function(value){ return value; },
parse: function(value){return value},
constraints : {
"range" : {
met_by : function(value){
return ((this._param.valA <= value) && (this._param.valB >= value));
}
},
"list" : {
met_by : function(value){
return (_.contains(this._param, value));
},
}
}
},
"BOOL":{
label: "bool",
isValid: function(value){return _.isBoolean(value);},
unParse: function(value){
if (value)
return 'true';
return 'false';
},
parse: function(value){
if (value.toLowerCase() === "true")
return true;
return false;
}
},
"NATURAL":{
label: "natural",
isValid: function(value){return utility.isInt(value);},
unParse: function(value){return value.toString();},
parse: function(value){return parseInt(value);}
},
"REAL":{
label: "real",
isValid: function(value){return utility.isReal(value); },
unParse: function(value){return value.toString(); },
parse: function(value){return parseFloat(value); }
},
"IP":{
label: "ip",
isValid: function(value){return (/^(\d{1,3}\.){3,3}\d{1,3}$/.test(value))},
unParse: function(value){ return value; },
parse: function(value){return value}
},
"IP6":{
label: "ip6",
isValid: function(value){return (/^[A-Fa-f0-9:]+$/.test(value)) },
unParse: function(value){return value;},
parse: function(value){return value}
},
"URL":{
label: "url",
isValid: function(value){return url.parse(value); },
unParse: function(value){ return value; },
parse: function(value){return value}
},
"TIME":{
label: "time",
isValid: function(value){return ((!_.isNaN(Date.parse(value))) || ( (value == time.TIME_PAST ) || (value == time.TIME_NOW ) || (value == time.TIME_FUTURE ) )); },
unParse: function(value){ return time.parse_time(value);},
parse: function(value){return time.parse_time(value); }
}
}
They can be used to refere to a specific primitive, instead of usgin the internal string
var pk = _.keys(PRIMITIVES);
Primitive.UNDEF = pk[0];
Primitive.STRING = pk[1];
Primitive.BOOL = pk[2];
Primitive.NATURAL = pk[3];
Primitive.REAL = pk[4];
Primitive.IP = pk[5];
Primitive.IP6 = pk[6];
Primitive.URL = pk[7];
Primitive.TIME = pk[8];
Primitive.VALUE_NONE = "*";
Primitive.prototype.setType = function(type){
Do we know about this type?
if ((!type) || !(PRIMITIVES[type]))
this.type = this.UNDEF;
else
this.type = type; // the type of primitive the istance is.
}
Primitive.knownPrimitive = function(prim){
return (_.indexOf(pk, prim.toUpperCase()) != -1);
}
Convert a string to a value; default implementation returns the string directly, returning None for the special string “*”, which represents “all values” in mPlane.
Primitive.prototype.parse = function(sval){
if (sval === Primitive.VALUE_NONE)
return Primitive.VALUE_NONE;
if ((this.type != Primitive.BOOL ) && (!sval)){
return null;
}
return (PRIMITIVES[this.type].parse(sval)) ;
}
Convert a value tp a string; IP address manipulation freely inspired by https://github.com/indutny/node-ip/blob/master/lib/ip.js
Primitive.prototype.unparse = function(val){
if ((this.type != Primitive.BOOL ) && (!val)){
return null;
}
return (PRIMITIVES[this.type].unParse(val)) ;
}
This is not in the reference implementation Checks if a value is valid for this type of primitive or not Call the primitive isValid defined in the PRIMITIVES obj
Primitive.prototype.isValid = function(val){
if ((this.type != Primitive.BOOL ) && (!val)){
return null;
}
return (PRIMITIVES[this.type].isValid(val)) ;
}
An Element represents a name for a particular type of data with a specific semantic meaning; it is analogous to an IPFIX Information Element, or a named column in a relational database.
An Element has a Name by which it can be compared to other Elements, and a primitive type, which it uses to convert values to and from strings.
The mPlane reference implementation includes a default registry of elements; use initialize_registry() to use these. An element can be create directly from a definition of the registry, passing the name of the registry element or from a json config
var Element = function(config){
If config is not defined, set default Values
if (!config || _.isUndefined(config)){
config = {};
}
if (_.isObject(config)){
this._name = config.name || "Undef";
(config.prim)?this._prim = config.prim.toUpperCase():this._prim = Primitive.UNDEF;
this._desc = config.desc || "";
}else{
var el = Element.getFromName(config);
this._name = el.name || "Undef";
this._prim = el.prim.toUpperCase() || Primitive.UNDEF;
this._desc = el.desc || "";
}
In this implementation contrains are part of the Element class This json contains the constraints defined for a specific instance Each constraint is identified by a unique id (the key in the json details) so that we can remove/change. The uniqueID is a simple incremental, so the uniqueID is used internally as the “next unique id”, while the constrains details are in the details json, with the uniqueID as key
this._constrains = { __uniqueID__ : 0,
details : {}};
}
Array containing the known elements
_element_registry = {};
Initialize the registry from a json file It is a static method, so it could be called without instantiate an element
Element.initialize_registry = function(filename){
var fn = filename || __DEFAULT_REGISTRY_FILE,
registry = "";
try {
registry = fs.readFileSync(fn);
_element_registry = JSON.parse(registry);
}
catch (err) {
console.log('There has been an error parsing the registry file.( '+fn+')');
console.log(err);
}
}
Return an element entry from the registry from the name Static method, so it could be called without instantiate an element
Element.getFromName = function(name){
var names = name.split(".");
var ret = _element_registry;
for (el in names){
if (ret[names[el]])
ret = ret[names[el]];
else
throw new Error("Undefined element in registry:"+name);
}
return ret;
}
Element.prototype.parse = function(sval){
this._prim.parse(sval);
}
Element.prototype.unparse = function(val){
this._prim.unparse(val);
}
Determine based on naming rules if this element is compatible with
element rval; that is, if transformation_to will return a function
for turning a value of this element to the other. Compatibility based
on name structure is a future feature; this method currently checks for
name equality only.
Element.prototype.compatible_with = function(rval){
return this._name == rval;
}
Returns a function which will transform values of this element
into values of element rval; used to support unit conversions.
This is a future feature, and is currently a no-op.
Only valid if compatible_with returns True.
Element.prototype.transformation_to = function(rval){
return function(rval){ return rval };
}
In this implementation, constraints are part of the Element class A constraint is a JSON obj with a type and optional/required parameters (depending on the type of contraint) The consequence is that we use a procedural approach intead of a pure obj oriented one
var Constraints = function(config){
this._prim = Primitive.UNDEF; // Default to UNDEF primitive type
this._name = "Constraint";
this._type = Constraints.UNDEF;
this.met_by = function(val){return true;}
this.single_value = function(){return false;}
if (_.isObject(config)){
if (config.prim && Primitive.knownPrimitive(config.prim))
this._prim=config.prim.toUpperCase();
if (config.name)
this._name = config.name;
if (config.type)
this._type = config.type;
if (config.param)
this._param = config.param;
}
Do we have specific functions for this primitive and this type of constrain?
if (PRIMITIVES[this._prim].constraints && PRIMITIVES[this._prim].constraints[this._type]){
if (PRIMITIVES[this._prim].constraints[this._type].met_by){
this.met_by = PRIMITIVES[this._prim].constraints[this._type].met_by;
}
if (PRIMITIVES[this._prim].constraints[this._type].single_value)
this.single_value = PRIMITIVES[this._prim][this._type].constraints.single_value;
}
}
Constraints.UNDEF = "undef";
Constraints.RANGE = "range";
Constraints.LIST = "list"; // List of acceptable values
Matches all constraints
Constraints.CONSTRAINT_ALL = "*"
Given a primitive and a string value, parse a constraint string into an instance of an appropriate Constraint class.
Constraints.parse_constraint = function(prim , sval){
if (_.isUndefined(prim) || _.isUndefined(sval))
throw new Error("Missing parameter in parse_constraints");
var ret = null; // If no case applies, return an UNDEF constraints
if (sval === Constraints.CONSTRAINT_ALL)
ret = Constraints.CONSTRAINT_ALL;
If the string contains a RANGE_SEP, it should be a range constraint
else if (sval.indexOf(RANGE_SEP) > 0){
var sp = sval.split(RANGE_SEP);
options = {
type: Constraints.RANGE,
prim: prim || Primitive.UNDEF,
name: "",
param: {valA: sp[0],
valB: sp[1]}
}
ret = new Constraints(options);
}
If the string contains a RANGE_SEP, it should be a list constraint
else if (sval.indexOf(SET_SEP) > 0){
var values = sval.split(SET_SEP);
options = {
type: Constraints.LIST,
prim: prim || Primitive.UNDEF,
name: "",
param: values
}
ret = new Constraints(options);
}
return ret || new Constraints();
}
Add a constraint to the Element. The constraint can be a json with a type and a number of optional/required parameters (depending on the type of contraint) or a string valid to be parsed by parse_constraint The prim of constraints is taken from the prim of the element The config is of type {type:
, params:{ }} Return the constrain internal id so that it ca be referred programmatically
Element.prototype.addConstraint = function(config){
var self = this , cons;
if (!_.isObject(config)){
cons = Constraints.parse_constraint(self._prim , config);
Is the prim compatible with ours?
if (cons._prim !== self._prim)
throw new Error("Error adding constraints. The contraints type is incompatible with element type");
}else{
if ((_.isUndefined(config.type))){
throw new Error('Constrain: missing param');
}
var params = config.constraintOptions || {};
var options = {
type: config.type,
prim: self._prim.toUpperCase(),
name: config.name,
param: params
}
cons = new Constraints(options);
}
ADD
this._constrains.details[this._constrains.__uniqueID__] = cons;
var ret = this._constrains.__uniqueID__;
this._constrains.__uniqueID__ += 1; // Next unique ID
return ret; // The internal ID
}
Clear the constraint identified by constraintsId or all constraints associated to the Element, if any
Element.prototype._clear_constraint = function(constraintId){
if (!_.isUndefined(constraintId))
delete this._constrains.details[constraintId];
else{
this._constrains.details = {};
this._constrains.__uniqueID__ = 0;
}
}
set the name of a specific constraint. Name uniqueness is not granted Usefull if I want to refer to a constraints by name
Element.prototype.setConstraintsName = function(constraintsId , name){
this._constrains.details[constraintsId]._name = name || 'Constraint';
}
Return a specific constraints set in element If constraintsId is undefined or null, return all the constraints
Element.prototype.getConstraints = function(constraintsId){
if (_.isUndefined(constraintsId))
return this._constrains.details;
else
return this._constrains.details[constraintsId];
}
checks is a value (of any type) matches a constraint set in Element If constraintsId is not defined or null, checks ALL the constraints It is a usefull shortcut to the constraints met_by function
Element.prototype.met_by = function(value , constraintsId){
if (_.isUndefined(constraintsId)){
var ret = true;
for (constr in this._constrains.details){
ret = ret && (this._constrains.details[constr].met_by(value));
It will never become true :>)
if (!ret)
return ret;
}
return ret;
}
else{
return(this._constrains.details[constraintsId].met_by(value));
}
}
Element.prototype.isValid = function(value){
return PRIMITIVES[this._prim].isValid(value);
}
////////////////////////////////////////////////////////////////////////////////////////////
A Parameter is an element which can take a constraint and a value. In Capabilities, Parameters have constraints and no value; in Specifications and Results, Parameters have both constraints and values. The creation of a parameter can be done directly through an Element instance, with the element name from the registry or with an instance of parameter
var Parameter = function(el , value){
if (el instanceof Element)
this._element = el;
else if (_.isString(el))
this._element = new Element(el);
if (!this._element)
throw new Error("Parameter el is not valid");
this.setValue(value);
}
Parameter.prototype.hasValue = function(){
return (!(_.isNull(this._value) || _.isUndefined(this._value)));
}
Set a specific value to a parameter. If the value does not meet the isValid func defined for the PAramter primitive and the defined constraints, throws an error
Parameter.prototype.setValue = function(val){
if (_.isUndefined(val))
return;
if ((this._element.isValid(val)) && (this._element.met_by(val)))
this._value = val;
else
throw new Error("Value is not valid for this parameter");
}
Parameter.prototype.getValue = function(){
return this._value;
}
Parameter.prototype.getConstraints = function(){
return this._element.getConstraints();
}
Parameter.prototype._clear_constraint = function(constraintId){
this._element._clear_constraint(constraintId);
}
Parameter.prototype.to_dict = function(){
return JSON.stringify(this);
}
A Metavalue is an element which can take an unconstrained value. Metavalues are used in statement metadata sections. Just do not set Constraints :>)
var Metavalue = Parameter;
A ResultColumn is an element which can take a collection of values (key:value). In Capabilities and Specifications, this collection is empty, while in Results it has one or more values, such that all the ResultColumns in the Result have the same number of values.
var ResultColumn = function(el , value){
this._vals = {};
if (el instanceof Element)
this._element = el;
else if (_.isString(el))
this._element = new Element(el);
if (!this._element)
this._element = new Element(); // Try to create a new undefined element
this.setValue(value);
}
Set a specific value to a ResultColumn. val MUST be a collection in the form {key:value,…} If one of the values does not meet the isValid func defined for the Element primitive and the defined constraints, throws an error
ResultColumn.prototype.setValue = function(val){
if (!val || _.isUndefined(val))
val = {};
if (_.isObject(val)){
for (el in val){
if ((this._element.isValid(val[el])) && (this._element.met_by(val[el])))
this._vals[el] = val[el];
else{
throw new Error("Value is not valid for this parameter");
}
}
}else
throw new Error("You should provide a json to ResultColumn.prototype.setValue");
}
ResultColumn.prototype.__setitem__ == ResultColumn.prototype.setValue;
ResultColumn.prototype.delValue = function(key){
if (this._vals[key])
delete this._vals[key];
}
ResultColumn.prototype.__delitem__ == ResultColumn.prototype.delValue;
ResultColumn.prototype.clear = function(){
this._vals = {};
}
ResultColumn.prototype.getValue = function(key){
if (_.isUndefined(key)){
return this._vals;
}
else
return this._vals[key];
}
A Statement is an assertion about the properties of a measurement or other action performed by an mPlane component. This class contains common implementation for the three kinds of mPlane statement. Implementations should use the mplane.Capability, mplane.Specification, mplane.Result Configuration can be taken from a dictionary, or with configuration paramenters
var Statement = function(config){
if (!config)
config = {};
if (config.dictval)
config = this._from_dict(config.dictval);
If we are importing from dict the names are the same as internal (with leading ), otherwise it is simpler to have no trailing
this._params = config.params || config._params || {};
this._metadata = config.metadata || config._metadata || {};
this._resultcolumns = config.resultcolumns || config._resultcolumns || {};
this._link = config.link || config._link || null;
this._verb = config.verb || config._verb || Statement.VERB_MEASURE;
this._label = config.label || config._label || null;
this._when = config.when || config._when || when_infinite;
this._token = config.token || config._token || null;
this._schedule = config.schedule || config._schedule || null; // completely ignored unless this is a specification
}
Statement.VERB_MEASURE = "measure";
Statement.VERB_QUERY = "query";
Statement.VERB_COLLECT = "collect";
Statement.VERB_STORE = "store";
Adds a parameter to the Statement, starting from a name, an element and value Constraints should be added to the element BEFORE adding to the Statement
Statement.prototype.add_parameter = function(parameter_name , element , value){
this._params[parameter_name] = new Parameter(element , value);
this._params[parameter_name]._element._name = parameter_name;
}
Statement.prototype.add_parameter_from_dict = function(dict , param_name){
var new_param = JSON.parse(dict);
if (! new_param instanceof Parameter)
throw new Error("The dictionary for the param is not valid");
this._params[param_name] = JSON.parse(dict);
}
Statement.prototype.has_parameter = function(parameter_name){
return(_.contains(_.keys(this._params),parameter_name));
}
Statement.prototype.parameter_names = function(){
return _.keys(this._params);
}
Statement.prototype.count_parameters = function(){
return (_.keys(this._params)).length;
}
Statement.prototype.get_parameter_value = function(param_name){
return this._params[param_name].getValue() || null;
}
Statement.prototype.get_parameter_constraints = function(param_name){
return this._params[param_name].getValue() || null;
}
Statement.prototype.set_parameter_value = function(param_name , value){
if (this._params[param_name])
this._params[param_name].setValue(value);
}
Statement.prototype.del_parameter = function(param_name){
if (this._params[param_name])
delete this._params[param_name];
}
MetaData are not structured so you can store any type of information and objects in metatdata
Statement.prototype.add_metadata = function(meta_name , value){
this._metadata[meta_name] = value || "";
}
Statement.prototype.has_metadata = function(meta_name ){
return(_.contains(_.keys(this._metadata),meta_name));
}
Statement.prototype.get_metadata_value = function(meta_name){
return(this._metadata[meta_name]);
}
Statement.prototype.metadata_names = function(){
return _.keys(this._metadata);
}
Statement.prototype.count_metadata = function(){
return _.keys(this._metadata).length;
}
Statement.prototype.set_metadata_value = function(meta_name , value){
if (this._metadata[meta_name])
this._metadata[meta_name]=value; // noextra check on metaData
}
Statement.prototype.add_result_column = function(elem_name , element){
this._resultcolumns[elem_name] = new ResultColumn(element);
}
Programatically set a ResultColumn. value should be a key,value obj
Statement.prototype.set_result_column_value = function(result_column , value){
this._resultcolumns[result_column].setValue(value);
}
Number of result rows in a specific resultColumn
Statement.prototype.count_result_rows = function(result_column){
return(_.keys(this._resultcolumns[result_column]._vals).length)
}
Statement.prototype.result_column_names = function(){
return _.keys(this._resultcolumns);
}
Statement.prototype.get_result_column_value = function(rc_name , key){
return this._resultcolumns[rc_name].getValue(key);
}
Statement.prototype.count_result_columns = function(rc_name , value){
return this.result_column_names().length;
}
Statement.prototype.has_result_columns = function(rc_name){
return(_.contains(_.keys(this._resultcolumns),rc_name));
}
Statement.prototype.get_link = function(){
return this._link;
}
Statement.prototype.set_link = function(link){
this._link = link;
}
Statement.prototype.get_label = function(){
return this._label;
}
Statement.prototype.set_label = function(label){
this._label = label;
}
Statement.prototype.get_when = function(){
return this._when;
}
Set the statement’s temporal scope. Ensures that the temporal scope is We can pass a When instance or directly a valid string for a when initialization
Statement.prototype.set_when = function(wh){
var w = null;
if (wh instanceof when)
w = wh;
else{
try{
w = new when(wh);
}catch(e){
If there an error in the parameter, set to now
w = new when(when.NOW);
}
}
this._when = w;
}
Return a hex string uniquely identifying the set of parameters and result columns (the schema) of this statement.
Statement.prototype._schema_hash = function(){
return md5("p " + " " + _.reduce(_.sortBy(this.parameter_names()) , function(concat, param){return(concat+param)})+ " r " + " " + _.reduce(_.sortBy(this.result_column_names()) , function(concat, rc){return(concat+rc)} ));
}
Return a hex string uniquely identifying the set of parameters, temporal scope, parameter values, and result columns of this statement. Used as a specification key.
Statement.prototype._pv_hash = function(){
var spk = _.sortBy(_.keys(this._params)),
spv = [];
for (k in spk){
spv.push(this.get_parameter_value(spk[k]));
}
tstr = this._verb + " w " + JSON.stringify(this._when) +
" pk " + " " + _.reduce(spk , function(concat, param){return(concat+param)}) +
" pv " + " " + _.reduce(spv , function(concat, param){return(concat+param)}) +
" r " + " " + _.reduce(_.sortBy(this.result_column_names()) , function(concat, param){return(concat+param)} );
return md5(tstr);
}
Return a hex string uniquely identifying the set of parameters, temporal scope, parameter constraints, parameter values, metadata, metadata values, and result columns (the extended specification) of this statement. Used as a complete token for statements.
Statement.prototype._mpcv_hash = function(){
var spk = _.sortBy(this.parameter_names()),
spv = [],
spc = [],
smk = _.sortBy(this.metadata_names()),
smv = [];
Parameters
for (k in spk){
spv.push(this.get_parameter_value(spk[k]));
spc.push(this._params[spk[k]].getConstraints());
}
Metadata
for (k in smk){
smv.push(this.get_metadata_value(smk[k]));
}
var tstr = this._verb + " w " + JSON.stringify(this._when) +
" pk " + " " + _.reduce(spk , function(concat, param){return(concat+param)}) +
" pc " + " " + _.reduce(spc , function(concat, param){return(concat+param)}) + " pv " + " " +
_.reduce(spv , function(concat, param){return(concat+param)})
" mk " + " " + _.reduce(smk , function(concat, param){return(concat+param)}) + " mv " + " " +
_.reduce(smv , function(concat, param){return(concat+param)})
" r " + " " + _.reduce(_.sortBy(this.result_column_names()) , function(concat, param){return(concat+param)} );
return md5(tstr);
}
Statement.prototype._default_token = function(){
return this._mpcv_hash();
}
Statement.prototype.get_token = function(){
if (!this._token)
this._token = this._default_token();
return this._token;
}
Statement.prototype._result_rows = function(resultColumn){
if (_.isUndefined(resultColumn))
throw new Error("resultColumn should be a valid column in _result_rows");
var rows = [],
rck = _.keys(this.get_result_column_value(resultColumn));
for (var i = 0 ; i< this.count_result_rows(resultColumn) ; i++){
rows.push(this._resultcolumns[resultColumn]._vals[rck[i]]);
}
return rows;
}
Statement.prototype.to_dict = function(){
return JSON.stringify(this);
}
Statement._from_dict = function(d){
return JSON.parse(d);
}
Statement.prototype.validate = function(){
var pn = this.parameter_names();
for (var i=0 ; i< this.count_parameters() ; i++){
if (!this._params[pn[i]].hasValue())
return false
}
return true;
}
Statement.prototype._clear_constraints = function(){
var pn = this.parameter_names();
for (var i=0 ; i< this.count_parameters() ; i++){
if (!this._params[pn[i]]._clear_constraint())
return false
}
}
CAPABILITY
A Capability represents something an mPlane component can do. Capabilities contain verbs (strings identifying the thing the component can do), parameters (which must be given by a client in a Specification in order for the component to do that thing), metadata (additional information about the process used to do that thing), and result columns (the data that thing will return).
Capabilities can either be created programatically, using the add_parameter(), add_metadata(), and add_result_column() methods, or by reading from a JSON or YAML object [FIXME document how this works once it’s written] The capability can be created starting from a Statement obj of from a valid JSON to create the new statement
var Capability = function(config){
Statement.apply( this, arguments ); // Call the Statement (super) contructor
}
util.inherits(Capability, Statement);
Capability.prototype.kind_str = function(){
return KIND_CAPABILITY;
}
Fill in parameters from a dictionary; used internally. The dictionary can contain any number of FULL DEFINED parameters The dictionary is intended to be JSON formatted, in this form {“parameter_name” : dict, …} FIXME: do some in deep test with this!
Capability.prototype._params_from_dict = function(d){
var pn = _.keys(d);
for (i=0 ; i<d.length ; i++){
Capability.add_parameter_from_dict(d[pd[i]]);
}
}
A Specification represents a request for an mPlane component to do
something it has advertised in a Capability.
Capabilities contain verbs (strings identifying the thing the
component can do), parameters (which must be given by a client
in a Specification in order for the component to do that thing),
metadata (additional information about the process used to do
that thing), and result columns (the data that thing will return).
Specifications are created either by passing a Capability the Specification is intended to use as the capability= argument of the constructor, or by reading from a JSON or YAML object [FIXME document how this works once it’s written]
var Specification = function(config){
Statement.apply( this, arguments ); // Call the Statement (super) contructor
this._schedule = config.schedule || sconfig._schedule || null;
if (config.capability){
We can have a serialized version (with trailing ) or a programmatically config, easier without _
this._verb = config.capability._verb || config.capability.verb || null;
this._label = config.capability._label || config.capability.label || null;
this._metadata = config.capability._metadata || config.capability.metadata || null;
this._params = config.capability._params || config.capability.params || null;
this._resultcolumns = config.capability._resultcolumns || config.capability.resultcolumns || null;
}
if (config.when || config._when)
this._when = config.when || config._when;
return this;// Chaining
}
util.inherits(Specification, Statement);
Specification.prototype.kind_str = function(){
return KIND_SPECIFICATION;
}
Specification.prototype._default_token = function(){
return this._pv_hash();
}
Specification.prototype.has_schedule = function(){
return (!_.isNull(this._schedule));
}
Specification.prototype.to_dict = function(){
return (JSON.stringify(this));
}
Return a Specification obj from a (valid) dictionary Throws an error in case the dictionary is not valid
Specification.from_dict = function(d){
var spec = JSON.parse(d);
if (spec instanceof Specification)
return spec;
else
throw new Error("The dictionary does not contain a valid Specification");
}
var Result = function(config){
Statement.apply( this, arguments ); // Call the Statement (super) contructor
if (config.capability){
We can have a serialized version (with trailing ) or a programmatically config, easier without _
this._verb = config.capability._verb || config.capability.verb || null;
this._label = config.capability._label || config.capability.label || null;
this._metadata = config.capability._metadata || config.capability.metadata || null;
this._params = config.capability._params || config.capability.params || null;
this._resultcolumns = config.capability._resultcolumns || config.capability.resultcolumns || null;
}
if (config.when || config._when)
this._when = config.when || config._when;
this.get_token();
allow parameters to take values other than constrained CheckME: i am not sure why this is needed…
this._clear_constraints();
return this;// Chaining
}
util.inherits(Result, Statement);
Result.prototype.kind_str = function(){
return KIND_RESULT;
}
Result.prototype.validate = function(){
var pn = this.parameter_names();
for (var i=0 ; i< this.count_parameters() ; i++){
if (!this._params[pn[i]].hasValue()){
return false
}
}
if (! this._when.is_definite()){
return false
}
return true;
}
Return a Result obj from a (valid) dictionary Throws an error in case the dictionary is not valid
Result.from_dict = function(d){
var spec = JSON.parse(d);
if (spec instanceof Result)
return spec;
else
throw new Error("The dictionary does not contain a valid Result");
}
Set a value for a specific column and key Notice that val should be in the form {key: value}
Result.prototype.set_result_value = function(columnName, val){
this._resultcolumns[columnName].setValue(val);
}
Get a value for a specific column and key If key is not specified, return all the rows for that specific column
Result.prototype.get_result_value = function(columnName, key){
return this._resultcolumns[columnName].getValue(key);
}
////////////////////////////////////////////////////////////////////////////////////////////
Notifications
////////////////////////////////////////////////////////////////////////////////////////////
Notifications are used to send additional information between mPlane clients and components other than measurement statements. Notifications can either be part of a normal measurement workflow (as Receipts and Redemptions) or signal exceptional conditions (as Withdrawals and Interrupts).
This class contains implementation common to all Notifications which do not contain any information from a related Capability or Specification.
var BareNotification = function(config){
if (!config)
config = {};
this._token = config.token || null;
return this; //Chainig
}
A Component sends an Exception to a Client, or a Client to a Component, to present a human-readable message about a failure or non-nominal condition
var Exception = function(config){
BareNotification.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
this._errmsg = config.errmsg || "Unspecified exception";
return this; //Chainig
}
util.inherits(Exception, BareNotification);
Exception.prototype.get_token = function(){
return this._token;
}
Exception.prototype.to_dict = function(){
return JSON.stringify(this);
}
Exception._from_dict(d){
var ret = JSON.parse(d);
if (! ret instanceof Exception)
throw new error("The dictionary should be a valid Exception instance");
else
return ret
}
Common implementation superclass for notifications that may contain all or part of a related Capability or Specification.
Clients and components should use mplane.Receipt, mplane.Redemption, and mplane.Withdrawal directly
var StatementNotification = function(config){
Statement.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
return this; //Chainig
}
util.inherits(StatementNotification, Statement);
Statement.prototype.to_dict = function(){
return JSON.stringify(this);
}
Statement._from_dict(d){
var ret = JSON.parse(d);
if (! ret instanceof Statement)
throw new error("The dictionary should be a valid Statement instance");
else
return ret
}
Statement.prototype.kind_str = function(){
throw new error("Cannot instantiate a raw StatementNotification");
}
A component presents a receipt to a Client in lieu of a result, when the result will not be available in a reasonable amount of time; or to confirm a Specification
var Receipt = function(config){
StatementNotification.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
return this; //Chainig
}
util.inherits(Receipt, StatementNotification);
Receipt.prototype.kind_str = function(){
return KIND_RECEIPT;
}
Receipt.prototype.validate = function(){
return this.validate();
}
A client presents a Redemption to a component from which it has received a Receipt in order to get the associated Result.
var Redemption = function(StatementNotification){
StatementNotification.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
if (!_.isUndefined(config.receipt))
this._token = receipt.get_token();
return this; //Chainig
}
util.inherits(Redemption, StatementNotification);
Redemption.protorype.kind_str = function(){
return KIND_REDEMPTION;
}
Redemption.protorype.validate = function(){
return this.validate();
}
var Withdrawal = function(config){
StatementNotification.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
return this; //Chainig
}
util.inherits(Withdrawal, StatementNotification);
Withdrawal.prototype.kind_str = function(){
return KIND_WITHDRAWAL;
}
Withdrawal.prototype.validate = function(){
return this.validate();
}
var Interrupt = function(){
StatementNotification.apply( this, arguments ); // Call the Statement (super) contructor
if (!config)
config = {};
return this; //Chainig
}
util.inherits(Interrupt, StatementNotification);
Interrupt.prototype.kind_str = function(){
return KIND_INTERRUPT;
}
Interrupt.prototype.validate = function(){
return this.validate();
}
////////////////////////////////////////////////////////////////////////////////////////////
A set of functions usefull for support tasks
var utility = function(){}
utility._parse_numset = function(valstr){
Iterate through each token from split and convert to integer
return _.map(valstr.split(SET_SEP) , function(value, index){
return parseInt(value);
} );
}
utility.parseNumSet = utility._parse_numset;
From an array of numbers to a string of values separated by SET_SEP
utility._unparse_numset = function(valset){
return (valset.join(SET_SEP))
}
utility.unParseNumSet = utility._unparse_numset;
utility._parse_wdayset = function(valstr){
return _.map(valstr.split(SET_SEP) , function(value, index){
return _dow_label.indexOf(value.toLocaleLowerCase().trim());
} );
}
utility.parseWdaySet = utility._parse_wdayset;
utility._unparse_wdayset = function(valset){
return _.map(valset , function(value, index){
return _dow_label[parseInt(value)];
} ).join(SET_SEP);
}
utility.unparseWdaySet = utility._unparse_wdayset;
utility.isInt = function(n) {
return (n % 1 === 0);
}
utility.isReal = function(n){
return ( !isNaN(parseFloat ( n )));
}
Exports
module.exports = {
time: time,
when: when,
When: when,
when_infinite: when_infinite,
utility: utility,
Scheduler: Scheduler,
Primitive : Primitive,
Element: Element,
Constraints: Constraints,
Parameter: Parameter,
Metavalue: Metavalue,
ResultColumn: ResultColumn,
Statement: Statement,
Capability: Capability,
Specification: Specification,
Result: Result,
Receipt: Receipt,
Redemption:Redemption,
Withdrawal: Withdrawal
}