cdif
Version:
Common device interconnect framework
310 lines (278 loc) • 9.46 kB
JavaScript
var events = require('events');
var util = require('util');
var validator = require('./validator');
var logger = require('./logger');
var CdifError = require('./error').CdifError;
var DeviceError = require('./error').DeviceError;
var Domain = require('domain');
function Service(device, serviceID, spec) {
this.device = device;
this.serviceID = serviceID;
this.serviceType = spec.serviceType;
this.actions = {};
this.states = {};
this.updateSpec(spec);
this.updateStateFromAction = this.updateStateFromAction.bind(this);
}
util.inherits(Service, events.EventEmitter);
Service.prototype.addAction = function(actionName, action) {
this.actions[actionName].invoke = action;
};
Service.prototype.updateSpec = function(spec) {
var actionList = spec.actionList;
for (var i in actionList) {
if (!this.actions[i]) {
var action = actionList[i];
this.actions[i] = {};
this.actions[i].args = action.argumentList; // save for validation
this.actions[i].invoke = null; // to be filled by device modules
}
}
// TODO: to save memory usage we can reclaim spec object and dynamically reconstruct it on get-spec call
var stateVariables = spec.serviceStateTable;
for (var i in stateVariables) {
if (!this.states[i]) {
this.states[i] = {};
if (stateVariables[i].dataType === 'object') {
this.states[i].variable = JSON.parse(JSON.stringify(stateVariables[i])); // save for schema deref
var schemaRef = stateVariables[i].schema;
if (schemaRef != null) {
var self = this.states[i].variable;
this.device.resolveSchemaFromPath(schemaRef, self, function(err, s, data) {
if (!err) {
s.schema = JSON.parse(JSON.stringify(data)); // reclaim doc object
} // or else this is still a pointer
});
}
} else {
this.states[i].variable = stateVariables[i];
}
//TODO: need to deep clone this if we reclaim spec obj
if (stateVariables[i].hasOwnProperty('defaultValue')) {
this.states[i].value = stateVariables[i].defaultValue;
} else {
this.states[i].value = '';
}
}
}
};
Service.prototype.getServiceStates = function(callback) {
var output = {};
for (var i in this.states) {
output[i] = this.states[i].value;
}
callback(null, output);
};
Service.prototype.setServiceStates = function(values, callback) {
var _this = this;
var errorMessage = null;
var updated = false;
var sendEvent = false;
var data = {};
if (typeof(values) !== 'object') {
errorMessage = 'event data must be object';
} else {
for (var i in values) {
if (this.states[i] === undefined) {
errorMessage = 'set invalid state for variable name: ' + i;
break;
}
}
}
if (errorMessage === null) {
for (var i in values) {
validator.validate(i, this.states[i].variable, values[i], function(err) {
if (!err) {
if (typeof(values[i]) === 'object') {
if (JSON.stringify(values[i]) !== JSON.stringify(_this.states[i].value)) {
updated = true;
_this.states[i].value = JSON.parse(JSON.stringify(values[i]));
}
} else {
if (_this.states[i].value !== values[i]) {
_this.states[i].value = values[i];
updated = true;
}
}
} else {
errorMessage = err.message;
}
});
if (errorMessage) break;
// report only eventable data
if (this.states[i].variable.sendEvents === true) {
if (typeof(values[i]) === 'object') {
data[i] = JSON.parse(JSON.stringify(values[i]));
} else {
data[i] = values[i];
}
}
}
}
if (errorMessage) {
callback(new CdifError('setServiceStates error: ' + errorMessage));
} else {
this.emit('serviceevent', updated, this.device.deviceID, this.serviceID, data);
callback(null);
}
};
Service.prototype.updateStateFromAction = function(action, input, output, callback) {
var updated = false;
var data = {};
for (var i in input) {
var argument = action.args[i];
if (argument == null) break;
var stateVarName = argument.relatedStateVariable;
if (stateVarName == null) break;
if (argument.direction === 'in') {
if (this.states[stateVarName].variable.sendEvents === true) {
if (this.states[stateVarName].value !== input[i]) {
data[stateVarName] = input[i];
updated = true;
}
}
this.states[stateVarName].value = input[i];
}
}
for (var i in output) {
var argument = action.args[i];
if (argument == null) break;
var stateVarName = argument.relatedStateVariable;
if (stateVarName == null) break;
if (argument.direction === 'out') {
if (this.states[stateVarName].variable.sendEvents === true) {
if (this.states[stateVarName].value !== output[i]) {
data[stateVarName] = output[i];
updated = true;
}
}
this.states[stateVarName].value = output[i];
}
}
callback(updated, data);
};
Service.prototype.validateActionCall = function(action, arguments, isInput, callback) {
var argList = action.args;
var failed = false;
var error = null;
if (arguments == null) {
return callback(new CdifError('no valid arguments'));
}
// argument keys must match spec
if (isInput) {
for (var i in argList) {
if (argList[i].direction === 'in') {
if (arguments[i] === undefined) {
failed = true;
error = new CdifError('missing argument: ' + i);
break;
}
}
}
} else {
for (var i in argList) {
if (argList[i].direction === 'out') {
if (arguments[i] === undefined) {
failed = true;
error = new CdifError('missing output argument: ' + i);
break;
}
}
}
}
if (failed) {
return callback(error);
}
// validate data
for (var i in arguments) {
var name = argList[i].relatedStateVariable;
var stateVar = this.states[name].variable;
if (isInput && argList[i].direction === 'out') {
// only check out args on call return
continue;
} else {
validator.validate(name, stateVar, arguments[i], function(err) {
if (err) {
error = new CdifError(err.message);
failed = true;
}
});
}
if (failed) break;
}
callback(error);
};
Service.prototype.invokeAction = function(actionName, input, callback) {
var _this = this;
var action = this.actions[actionName];
if (action === undefined) {
return callback(new CdifError('action not found: ' + actionName), null);
}
if (input === undefined) {
return callback(new CdifError('cannot identify input arguments'), null);
}
if (action.invoke === null) {
return callback(new DeviceError('action: ' + actionName + ' not implemented'), null);
}
this.validateActionCall(action, input, true, function(err) {
if (err) {
return callback(err, null);
}
var unsafeDomain = Domain.create();
unsafeDomain.on('error', function(err) {
logger.error(err);
return callback(err, null);
});
unsafeDomain.run(function() {
action.invoke(input, function(err, output) {
if (err) {
//TODO: validate the content of fault object according to its optional fault definition in device spec
// API's formal fault definition, which can be in either simple or complex type, would make it more conformant to WSDL
if (output && output.fault) {
return callback(new DeviceError(err.message), output.fault);
}
return callback(new DeviceError(err.message), null);
}
_this.validateActionCall(action, output, false, function(error) {
if (error) {
return callback(error, null);
}
_this.updateStateFromAction(action, input, output, function(updated, data) {
_this.emit('serviceevent', updated, _this.device.deviceID, _this.serviceID, data);
});
callback(null, output);
});
});
});
});
};
Service.prototype.setEventSubscription = function(subscribe, unsubscribe) {
this.subscribe = subscribe;
this.unsubscribe = unsubscribe;
};
Service.prototype.subscribeEvent = function(onChange, callback) {
if (this.subscribe) {
this.subscribe(onChange, function(err) {
if (err) {
return callback(new DeviceError('event subscription failed: ' + err.message));
}
callback(null);
});
} else {
// we can still send state change events upon action call
callback(null);
}
};
Service.prototype.unsubscribeEvent = function(callback) {
if (this.unsubscribe) {
this.unsubscribe(function(err) {
if (err) {
return callback(new DeviceError('event unsubscription failed: ' + err.message));
}
callback(null);
});
} else {
callback(null);
}
};
module.exports = Service;