diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index eca0c1dc7..fe7338fdf 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -3,3 +3,4 @@ Add NGSIv2 metadata support to device provisioned attributes Fix: Error message when sending measures with unknown/undefined attribute Add Null check within executeWithSecurity() to avoid crash (#829) Add NGSIv2 metadata support to attributeAlias plugin. +Add support for JEXL as expression language (#801) diff --git a/README.md b/README.md index e3499e6e7..76f51a3df 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ decommission devices. ## Testing -Contribitions to development can be found [here](doc/development.md) - additional contributions are welcome. +Contributions to development can be found [here](doc/development.md) - additional contributions are welcome. ### Agent Console diff --git a/doc/expressionLanguage.md b/doc/expressionLanguage.md index 362bc5d5d..394c53aad 100644 --- a/doc/expressionLanguage.md +++ b/doc/expressionLanguage.md @@ -16,8 +16,8 @@ The IoTAgent Library provides an expression language for measurement transformation, that can be used to adapt the information coming from the South Bound APIs to the information reported to the Context Broker. Expressions in this -language can be configured for provisioned attributes as explained in the Device Provisioning API section in the main -README.md. +language can be configured for provisioned attributes as explained in the Device Provisioning API section in the +main README.md. ## Measurement transformation @@ -301,3 +301,94 @@ two possible types of expressions: Integer (arithmetic operations) or Strings. - update (of type "Boolean"): true -> ${@update * 20} -> ${ 1 \* 20 } -> $ { 20 } -> $ { "20"} -> False - update (of type "Boolean"): false -> ${@update * 20} -> ${ 0 \* 20 } -> $ { 0 } -> $ { "0"} -> False - update (of type "Boolean"): true -> ${trim(@updated)} -> ${trim("true")} -> $ { "true" } -> $ { "true"} -> True + +## JEXL Based Transformations + +As an alternative, the IoTAgent Library supports as well [JEXL](https://github.com/TomFrost/jexl). +To use JEXL, you will need to either configure it as default +language using the `defaultExpressionLanguage` field to `jexl` +(see [configuration documentation](installationguide.md)) or configuring +the usage of JEXL as expression language for a given device: + +``` +{ + "devices":[ + { + "device_id":"45", + "protocol":"GENERIC_PROTO", + "entity_name":"WasteContainer:WC45", + "entity_type":"WasteContainer", + "expressionLanguage": "jexl", + "attributes":[ + { + "name":"location", + "type":"geo:point", + "expression": "..." + }, + { + "name":"fillingLevel", + "type":"Number", + "expression": "..." + } + ] + } + ] +} +``` + +In the following we provide examples of using JEXL to apply transformations. + +### Quick comparison to default language + +* JEXL supports the following types: Boolean, String, Number, Object, Array. +* JEXL allows to navigate and [filter](https://github.com/TomFrost/jexl#collections) objects and arrays. +* JEXL supports if..then...else... via [ternary operator](https://github.com/TomFrost/jexl#ternary-operator). +* JEXL additionally supports the following operations: + Divide and floor `//`, Modulus `%`, Logical AND `&&` and + Logical OR `||`. Negation operator is `!` +* JEXL supports [comparisons](https://github.com/TomFrost/jexl#comparisons). + + +For more details, check JEXL language details +[here](https://github.com/TomFrost/jexl#all-the-details). + +### Examples of expressions + +The following table shows expressions and their expected outcomes taking into +account the following measures at southbound interface: + +* `value` with value 6 (number) +* `name` with value `"DevId629"` (string) +* `object` with value `{name: "John", surname: "Doe"}` (JSON object) +* `array` with value `[1, 3]` (JSON Array) + + +| Expression | Expected outcome | +|:--------------------------- |:----------------------- | +| `5 * value` | `30` | +| `(6 + value) * 3` | `36` | +| `value / 12 + 1` | `1.5` | +| `(5 + 2) * (value + 7)` | `91` | +| `value * 5.2` | `31.2` | +| `"Pruebas " + "De Strings"` | `"Pruebas De Strings"` | +| `name + "value is " +value` | `"DevId629 value is 6"` | + +Support for `trim`, `length`, `substr` and `indexOf` transformations was added. + +| Expression | Expected outcome | +|:--------------------------- |:----------------------- | +| " a "|trim | `a` | +| name|length | `8` | +| name|indexOf("e")| `1` | +| name|substring(0,name|indexOf("e")+1)| `"De"` | + +The following are some expressions not supported by the legacy expression +language: + +| Expression | Expected outcome | +|:----------------------------------- |:----------------------- | +| `value == 6? true : false` | `true` | +| value == 6 && name|indexOf("e")>0 | `true` | +| `array[1]+1` | `3` | +| `object.name` | `"John"` | +| `{type:"Point",coordinates: [value,value]}`| `{type:"Point",coordinates: [6,6]}` | diff --git a/doc/installationguide.md b/doc/installationguide.md index cc9092e14..5072e0942 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -210,6 +210,10 @@ used for the same purpose. For instance: the IoTAgent runs in a single thread. For more details about multi-core functionality, please refer to the [Cluster](https://nodejs.org/api/cluster.html) module in Node.js and [this section](howto.md#iot-agent-in-multi-thread-mode) of the library documentation. +- **defaultExpressionLanguage**: the default expression language used to + compute expressions, possible values are: `legacy` or `jexl`. When not set or + wrongly set, `legacy` is used as default value. + ### Configuration using environment variables @@ -262,3 +266,4 @@ overrides. | IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` | | IOTA_AUTOCAST | `autocast` | | IOTA_MULTI_CORE | `multiCore` | +| IOTA_DEFAULT_EXPRESSION_LANGUAGE | defaultExpressionLanguage | diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 13309ef09..c6dc5bc3f 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -91,7 +91,8 @@ function processEnvironmentVariables() { 'IOTA_APPEND_MODE', 'IOTA_POLLING_EXPIRATION', 'IOTA_POLLING_DAEMON_FREQ', - 'IOTA_MULTI_CORE' + 'IOTA_MULTI_CORE', + 'IOTA_DEFAULT_EXPRESSION_LANGUAGE' ], iotamVariables = [ 'IOTA_IOTAM_URL', @@ -322,6 +323,11 @@ function processEnvironmentVariables() { } else { config.multiCore = config.multiCore === true; } + + if (process.env.IOTA_DEFAULT_EXPRESSION_LANGUAGE) { + config.defaultExpressionLanguage = process.env.IOTA_DEFAULT_EXPRESSION_LANGUAGE; + } + } function setConfig(newConfig) { diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index c315a8a8b..a045e9961 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -84,6 +84,16 @@ function doActivate(newConfig, callback) { commandRegistry, securityService; + logger.format = logger.formatters.pipe; + + config.setConfig(newConfig); //moving up here because otherwise env variable are not considered by the code below + + if (!config.getConfig().dieOnUnexpectedError) { + process.on('uncaughtException', globalErrorHandler); + } + + newConfig = config.getConfig(); + if (newConfig.contextBroker) { if (! newConfig.contextBroker.url && newConfig.contextBroker.host && newConfig.contextBroker.port) { newConfig.contextBroker.url = 'http://' + newConfig.contextBroker.host + ':' + newConfig.contextBroker.port; @@ -113,7 +123,16 @@ function doActivate(newConfig, callback) { } } - config.setConfig(newConfig); + if (newConfig.defaultExpressionLanguage && + (newConfig.defaultExpressionLanguage === 'legacy' || + newConfig.defaultExpressionLanguage ==='jexl')){ + logger.info(context, 'Using ' + newConfig.defaultExpressionLanguage + ' as default expression language'); + } else { + logger.info(context, 'Default expression language not set, or invalid, using legacy configuration'); + newConfig.defaultExpressionLanguage = 'legacy'; + } + + config.setConfig(newConfig); //after chaging some configuration, we re apply the configuration logger.info(context, 'Activating IOT Agent NGSI Library.'); @@ -156,10 +175,6 @@ function doActivate(newConfig, callback) { ], callback); }; - if (!config.getConfig().dieOnUnexpectedError) { - process.on('uncaughtException', globalErrorHandler); - } - config.setSecurityService(securityService); config.setRegistry(registry); config.setGroupRegistry(groupRegistry); diff --git a/lib/model/Device.js b/lib/model/Device.js index a367126e3..071dfc4c8 100644 --- a/lib/model/Device.js +++ b/lib/model/Device.js @@ -48,7 +48,8 @@ var Device = new Schema({ internalId: String, creationDate: { type: Date, default: Date.now }, internalAttributes: Object, - autoprovision: Boolean + autoprovision: Boolean, + expressionLanguage: String }); function load(db) { diff --git a/lib/model/Group.js b/lib/model/Group.js index 447524d4c..8754981b6 100644 --- a/lib/model/Group.js +++ b/lib/model/Group.js @@ -41,7 +41,8 @@ var Group = new Schema({ lazy: Array, attributes: Array, internalAttributes: Array, - autoprovision: Boolean + autoprovision: Boolean, + expressionLanguage: String }); function load(db) { diff --git a/lib/plugins/expressionPlugin.js b/lib/plugins/expressionPlugin.js index 8ed729556..f4224886e 100644 --- a/lib/plugins/expressionPlugin.js +++ b/lib/plugins/expressionPlugin.js @@ -22,12 +22,14 @@ * please contact with::daniel.moranjimenez@telefonica.com * * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Federico M. Facca - Martel Innovate */ 'use strict'; var _ = require('underscore'), - parser = require('./expressionParser'), + legacyParser = require('./expressionParser'), + jexlParser = require('./jexlParser'), config = require('../commonConfig'), /*jshint unused:false*/ logger = require('logops'), @@ -62,7 +64,27 @@ function mergeAttributes(attrList1, attrList2) { function update(entity, typeInformation, callback) { + + function checkJexl(typeInformation){ + if (config.getConfig().defaultExpressionLanguage === 'jexl' && + typeInformation.expressionLanguage && + typeInformation.expressionLanguage !== 'legacy') { + return true; + } else if (config.getConfig().defaultExpressionLanguage === 'jexl' && + !typeInformation.expressionLanguage) { + return true; + } else if (config.getConfig().defaultExpressionLanguage === 'legacy' && + typeInformation.expressionLanguage && typeInformation.expressionLanguage === 'jexl') { + return true; + } + return false; + } + function processEntityUpdateNgsi1(entity) { + var parser = legacyParser; + if (checkJexl(typeInformation)){ + parser = jexlParser; + } var expressionAttributes = [], ctx = parser.extractContext(entity.attributes); @@ -76,6 +98,10 @@ function update(entity, typeInformation, callback) { } function processEntityUpdateNgsi2(attributes) { + var parser = legacyParser; + if (checkJexl(typeInformation)){ + parser = jexlParser; + } var expressionAttributes = [], ctx = parser.extractContext(attributes); diff --git a/lib/plugins/jexlParser.js b/lib/plugins/jexlParser.js new file mode 100644 index 000000000..176b1feb8 --- /dev/null +++ b/lib/plugins/jexlParser.js @@ -0,0 +1,188 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Developed by: Federico M. Facca - Martel Innovate + */ + +'use strict'; + +const jexl = require('jexl'); +var errors = require('../errors'), + logger = require('logops'), + config = require('../commonConfig'), + logContext = { + op: 'IoTAgentNGSI.JEXL' + }; + + +jexl.addTransform('indexOf', (val, char) => String(val).indexOf(char)); +jexl.addTransform('length', (val) => String(val).length); +jexl.addTransform('trim', (val) => String(val).trim()); +jexl.addTransform('substr', (val, int1, int2) => String(val).substr(int1, int2)); + +function parse(expression, context, callback) { + var result, + error; + + try { + + result = jexl.evalSync (expression, context); + + } catch (e) { + error = new errors.InvalidExpression(expression); + if (callback) { + callback(error); + } else { + throw error; + } + } + + if (callback) { + callback(null, result); + } else { + return result; + } +} + +function extractContext(attributeList) { + var context = {}; + var value; + + for (var i = 0; i < attributeList.length; i++) { + if (Number.parseInt(attributeList[i].value)) { + value = Number.parseInt(attributeList[i].value); + } else if (Number.parseFloat(attributeList[i].value)) { + value = Number.parseFloat(attributeList[i].value); + } else if (String(attributeList[i].value) === 'true') { + value = true; + } else if (String(attributeList[i].value) === 'false') { + value = false; + } else { + value = attributeList[i].value; + } + if(attributeList[i].name) { + context[attributeList[i].name] = value; + } + /*jshint camelcase: false */ + if(attributeList[i].object_id) { + context[attributeList[i].object_id] = value; + } + /*jshint camelcase: true */ + } + + return context; +} + +function applyExpression(expression, context, typeInformation) { + return jexl.evalSync (expression, context); +} + +function expressionApplier(context, typeInformation) { + return function(attribute) { + + /** + * Determines if a value is of type float + * + * @param {String} value Value to be analyzed + * @return {boolean} True if float, False otherwise. + */ + function isFloat(value) { + return !isNaN(value) && value.toString().indexOf('.') !== -1; + } + + var newAttribute = { + name: attribute.name, + type: attribute.type + }; + + /*jshint camelcase: false */ + if (config.checkNgsi2() && attribute.object_id) { + newAttribute.object_id = attribute.object_id; + } + + newAttribute.value = applyExpression(attribute.expression, context, typeInformation); + + if (attribute.type === 'Number' && isFloat(newAttribute.value)) { + newAttribute.value = Number.parseFloat(newAttribute.value); + } + else if (attribute.type === 'Number' && Number.parseInt(newAttribute.value)) { + newAttribute.value = Number.parseInt(newAttribute.value); + } + else if (attribute.type === 'Boolean') { + newAttribute.value = (newAttribute.value === 'true' || newAttribute.value === '1'); + } + else if (attribute.type === 'None') { + newAttribute.value = null; + } + else if (attribute.type === 'Text') { + newAttribute.value = String(newAttribute.value); + } + else if (attribute.type !== 'StructuredValue') { + newAttribute.value = String(newAttribute.value); + } + + return newAttribute; + }; +} + +function isTransform(identifier) { + return jexl.getTransform(identifier) === ( null || undefined) ? false : true; +} + +function contextAvailable(expression, context) { + var error; + try{ + var lexer = jexl._getLexer(); + var identifiers = lexer.tokenize(expression).filter(function(token) { + return token.type === 'identifier'; + }); + var keys = Object.keys(context); + var validContext = true; + identifiers.some(function(element) { + if (!keys.includes(element.value) && !isTransform(element.value)) { + validContext = false; + logger.warn(logContext, 'For expression "[%s]" context does not have element %s' , expression, element.value); + } + return validContext === false; + }); + if(validContext) { + jexl.evalSync (expression, context); + } + return validContext; + } catch (e) { + error = new errors.InvalidExpression(expression); + throw error; + } +} + +function processExpressionAttributes(typeInformation, list, context) { + return list + .filter(function(item) { + return item.expression && contextAvailable(item.expression, context); + }) + .map(expressionApplier(context, typeInformation)); +} + +exports.extractContext = extractContext; +exports.processExpressionAttributes = processExpressionAttributes; +exports.applyExpression = applyExpression; +exports.parse = parse; diff --git a/lib/templates/createDevice.json b/lib/templates/createDevice.json index 07041c4a1..927e63ad6 100644 --- a/lib/templates/createDevice.json +++ b/lib/templates/createDevice.json @@ -36,6 +36,10 @@ "description": "Transport protocol used by the platform to communicate with the device", "type": "string" }, + "expressionLanguage": { + "description": "Expression language used to apply expressions for this device", + "type": "string" + }, "lazy": { "description": "list of lazy attributes of the devices", "type": "array", @@ -89,8 +93,7 @@ }, "expression": { "description": "Optional expression for measurement transformation", - "type": "string", - "pattern": "^([^<>;'=]+)+$" + "type": "string" }, "entity_name": { "description": "Optional entity name for multientity updates", diff --git a/lib/templates/deviceGroup.json b/lib/templates/deviceGroup.json index 0816012a6..8c9279cdb 100644 --- a/lib/templates/deviceGroup.json +++ b/lib/templates/deviceGroup.json @@ -37,6 +37,10 @@ "description": "list of lazy attributes of the devices", "type": "array" }, + "expressionLanguage": { + "description": "Expression language used to for the group of devices", + "type": "string" + }, "attributes": { "description": "list of active attributes of the devices", "type": "array" diff --git a/lib/templates/updateDevice.json b/lib/templates/updateDevice.json index c8e5f71e4..4a0acee76 100644 --- a/lib/templates/updateDevice.json +++ b/lib/templates/updateDevice.json @@ -81,8 +81,7 @@ }, "expression": { "description": "Optional expression for measurement transformation", - "type": "string", - "pattern": "^([^<>;'=]+)+$" + "type": "string" }, "entity_name": { "description": "Optional entity name for multientity updates", diff --git a/package.json b/package.json index 61dd653fd..dae453131 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "body-parser": "~1.19.0", "command-shell-lib": "1.0.0", "express": "~4.16.4", + "jexl": "2.1.1", "jison": "0.4.18", "logops": "2.1.0", "moment": "~2.24.0", diff --git a/test/unit/expressions/expressionCombinedTransformations-test.js b/test/unit/expressions/expressionCombinedTransformations-test.js new file mode 100644 index 000000000..8132d9474 --- /dev/null +++ b/test/unit/expressions/expressionCombinedTransformations-test.js @@ -0,0 +1,285 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../lib/fiware-iotagent-lib'), + utils = require('../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfigJexl = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'legacy', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }, + iotAgentConfigLegacy = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'jexl', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Combine Jexl and legacy expressions (default JEXL)', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigJexl, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy"', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL" - default', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); + +describe('Combine Jexl and legacy expressions (default Legacy)', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigLegacy, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy" - default', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL"', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); diff --git a/test/unit/expressions/jexlBasedTransformations-test.js b/test/unit/expressions/jexlBasedTransformations-test.js new file mode 100644 index 000000000..31f74cd0e --- /dev/null +++ b/test/unit/expressions/jexlBasedTransformations-test.js @@ -0,0 +1,305 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../lib/fiware-iotagent-lib'), + utils = require('../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'Light': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + } + ] + }, + 'LightError': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * / 20' + } + ] + }, + 'WeatherStation': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + expression: 'pressure * 20' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity / 2) + " and pressure " + (pressure * 20)' + } + ] + }, + 'WeatherStationMultiple': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure25', + type: 'Hgmm', + expression: 'pressure * 20' + }, + { + object_id: 'h', + name: 'humidity12', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity12 / 2) + " and pressure " + (pressure25 * 20)' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Javascript Expression Language (JEXL) based transformations plugin ', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for attributes with expressions', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin1.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin1Success.json')); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for expressions with syntax errors', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin4.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin1Success.json')); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'LightError', '', values, function(error) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + error.code.should.equal(400); + done(); + }); + }); + }); + + describe('When there are expression attributes that are just calculated (not sent by the device)', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin2.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin2Success.json')); + }); + + it('should calculate them and add them to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an expression with multiple variables with numbers arrive', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin5.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin5Success.json')); + }); + + it('should calculate it and add it to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a measure arrives and there is not enough information to calculate an expression', function() { + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v1/updateContext', utils.readExampleFile( + './test/unit/examples/contextRequests/updateContextExpressionPlugin3.json')) + .reply(200, utils.readExampleFile( + './test/unit/examples/contextResponses/updateContextExpressionPlugin3Success.json')); + }); + + it('should not calculate the expression', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/expressions/jexlExpression-test.js b/test/unit/expressions/jexlExpression-test.js new file mode 100644 index 000000000..fd82c4de3 --- /dev/null +++ b/test/unit/expressions/jexlExpression-test.js @@ -0,0 +1,240 @@ +/* + * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + */ +'use strict'; + +var should = require('should'), + expressionParser = require('../../../lib/plugins/jexlParser'); + +describe('Jexl expression interpreter', function() { + var arithmetic, + scope = { + value: 6, + other: 3, + theString: '12.6, -19.4', + spaces: '5 a b c d 5', + big: 2000, + number: 145, + number2: 155, + number3inside: 200, + array: [1,2], + object: { + name: 'John', + surname: 'Doe' + } + }; + + describe('When a expression with a single value is parsed', function() { + it('should return that value', function(done) { + expressionParser.parse('5 * value', scope, function(error, result) { + should.not.exist(error); + result.should.equal(30); + done(); + }); + }); + }); + + arithmetic = [ + ['5 * value', 30], + ['(6 + value) * 3', 36], + ['value / 12 + 1', 1.5], + ['(5 + 2) * (value + 7)', 91], + ['(5 - other) * (value + 7)', 26], + ['3 * 5.2', 15.6], + ['value * 5.2', 31.2] + ]; + + function arithmeticUseCase(arithmeticExpr) { + describe('When an expression with the arithmetic operation [' + arithmeticExpr[0] + '] is parsed', function() { + it('should be interpreted appropriately', function(done) { + expressionParser.parse(arithmeticExpr[0], scope, function(error, result) { + should.not.exist(error); + result.should.approximately(arithmeticExpr[1], 0.000001); + done(); + }); + }); + }); + } + + for (var i = 0; i < arithmetic.length; i++) { + arithmeticUseCase(arithmetic[i]); + } + + describe('When an expression with two strings is concatenated', function() { + it('should return the concatenation of both strings', function(done) { + expressionParser.parse('"Pruebas" + "DeStrings"', scope, function(error, result) { + should.not.exist(error); + result.should.equal('PruebasDeStrings'); + done(); + }); + }); + }); + + describe('When string transformation functions are executed', function() { + it('should return the appropriate piece of the string', function(done) { + expressionParser.parse( + 'theString|substr(theString|indexOf(\",\") + 1, theString|length)|trim', + scope, + function(error, result) { + should.not.exist(error); + result.should.equal('-19.4'); + done(); + }); + }); + }); + + describe('When an expression contains variables with numbers', function() { + it('should return the appropriate result', function(done) { + expressionParser.parse('number + number2 + number3inside', + scope, function(error, result) { + should.not.exist(error); + result.should.equal(500); + done(); + }); + }); + }); + + describe('When an expression contains multiple parenthesis', function() { + it('should return the appropriate result', function(done) { + expressionParser.parse('((number) * (number2))', + scope, function(error, result) { + should.not.exist(error); + result.should.equal(22475); + done(); + }); + }); + }); + + describe('When trim() function is executed', function() { + it('should return the appropriate piece of the string', function(done) { + expressionParser.parse( + 'spaces|trim', + scope, + function(error, result) { + should.not.exist(error); + result.should.equal('5 a b c d 5'); + done(); + }); + }); + }); + + describe('When an expression with strings containing spaces is concatenated', function() { + it('should honour the whitespaces', function(done) { + expressionParser.parse('"Pruebas " + "De Strings"', scope, function(error, result) { + should.not.exist(error); + result.should.equal('Pruebas De Strings'); + done(); + }); + }); + }); + + describe('When an expression with strings with single quotation marks is parsed', function() { + it('should accept the strings', function(done) { + expressionParser.parse('\'Pruebas \' + \'De Strings\'', scope, function(error, result) { + should.not.exist(error); + result.should.equal('Pruebas De Strings'); + done(); + }); + }); + }); + + describe('When a string is concatenated with a number', function() { + it('should result in a string concatenation', function(done) { + expressionParser.parse('"number " + 5', scope, function(error, result) { + should.not.exist(error); + result.should.equal('number 5'); + done(); + }); + }); + }); + + describe('When an expression with a parse error is parsed', function() { + it('should raise an INVALID_EXPRESSION error', function(done) { + expressionParser.parse('"numb+sd ññ ((', scope, function(error, result) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + done(); + }); + }); + }); + + describe('When an string function is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('(24 * big)|indexOf("80")', scope, function(error, result) { + should.not.exist(error); + result.should.equal(1); + done(); + }); + }); + }); + + describe('When an ternary operator is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('value == 6? true : false', scope, function(error, result) { + should.not.exist(error); + result.should.equal(true); + done(); + }); + }); + }); + + describe('When an logic operator is used with an expression', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('value == 6 && spaces|indexOf("a")>0', scope, function(error, result) { + should.not.exist(error); + result.should.equal(true); + done(); + }); + }); + }); + + describe('When a function is applied to an array', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('array[1]+1', scope, function(error, result) { + should.not.exist(error); + result.should.equal(3); + done(); + }); + }); + }); + + describe('When a function is applied to an object', function() { + it('should work on the expression value', function(done) { + expressionParser.parse('object.name', scope, function(error, result) { + should.not.exist(error); + result.should.equal('John'); + done(); + }); + }); + }); + + describe('When an expression aims at creating an object', function() { + it('it should work', function(done) { + expressionParser.parse('{type:"Point",coordinates: [value,other]}', scope, function(error, result) { + should.not.exist(error); + result.should.deepEqual({type:'Point',coordinates: [6,3]}); + done(); + }); + }); + }); + +}); diff --git a/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js b/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js new file mode 100644 index 000000000..212025326 --- /dev/null +++ b/test/unit/ngsiv2/expressions/expressionCombinedTransformations-test.js @@ -0,0 +1,287 @@ +/* + * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../../lib/fiware-iotagent-lib'), + utils = require('../../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfigJexl = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041 + }, + defaultExpressionLanguage: 'jexl', + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'legacy', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }, + iotAgentConfigLegacy = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041 + }, + types: { + 'WeatherStationLegacy': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * 20}' + } + ] + }, + 'WeatherStationJexl': { + commands: [], + type: 'WeatherStation', + expressionLanguage: 'jexl', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Combine Jexl and legacy expressions (default JEXL) - NGSI v2', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigJexl, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy"', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL" - default', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws2/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws2', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); + +describe('Combine Jexl and legacy expressions (default Legacy) - NGSI v2', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfigLegacy, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for type with expression "legacy" - default', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws3/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the legacy expression before sending the values', function(done) { + iotAgentLib.update('ws3', 'WeatherStationLegacy', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for type with expression "JEXL"', function() { + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws4/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the default (JEXL) expression before sending the values', function(done) { + iotAgentLib.update('ws4', 'WeatherStationJexl', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +}); diff --git a/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js new file mode 100644 index 000000000..aedf953e4 --- /dev/null +++ b/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js @@ -0,0 +1,854 @@ +/* + * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Developed by: Federico M. Facca - Martel Innovate + */ +'use strict'; + +/* jshint camelcase: false */ + +var iotAgentLib = require('../../../../lib/fiware-iotagent-lib'), + utils = require('../../../tools/utils'), + should = require('should'), + logger = require('logops'), + nock = require('nock'), + contextBrokerMock, + iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + defaultExpressionLanguage: 'jexl', + server: { + port: 4041 + }, + types: { + 'Light': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + }, + { + object_id: 'm', + name: 'manufacturer', + type: 'Object', + }, + { + object_id: 'r', + name: 'revisions', + type: 'Array', + }, + { + object_id: 'x', + name: 'consumption_x', + type: 'Number', + expression: 'pressure * 20' + } + ] + }, + 'LightError': { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * / 20' + } + ] + }, + 'WeatherStation': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure * 20' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: 'consumption * 20' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity / 2) + " and pressure " + (pressure * 20)' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: 'alive * 20' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: 'updated * 20' + }, + ] + }, + 'WeatherStationMultiple': { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: 'pressure|trim' + }, + { + object_id: 'p25', + name: 'pressure25', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: 'consumption|trim' + }, + { + object_id: 'h', + name: 'humidity12', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: '"Humidity " + (humidity12 / 2) + " and pressure " + (pressure25 * 20)' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: 'alive|trim' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: 'updated|trim' + }, + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' + }; + +describe('Java expression language (JEXL) based transformations plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for expressions with syntax errors', function() { + // Case: Update for an attribute with bad expression + var values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'LightError', '', values, function(error) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + error.code.should.equal(400); + done(); + }); + }); + }); + + describe('When there are expression attributes that are just calculated (not sent by the device)', function() { + // Case: Expression which results is sent as a new attribute + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin2.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should calculate them and add them to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an expression with multiple variables with numbers arrive', function() { + // Case: Update for integer and string attributes with expression + + var values = [ + { + name: 'p25', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin4.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should calculate it and add it to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type integer', function() { + // Case: Update for an integer attribute without expression + var values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin11.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type integer', function() { + // Case: Update for an integer attribute with arithmetic expression + var values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expression and type integer', function() { + // Case: Update for an integer attribute with string expression + var values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin11.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type float', function() { + // Case: Update for a Float attribute without expressions + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin3.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type float', function() { + // Case: Update for a Float attribute with arithmetic expression + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin8.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and type float', function() { + // Case: Update for a Float attribute with string expression + + var values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin3.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and NULL type', function() { + // Case: Update for a Null attribute without expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and NULL type', function() { + // Case: Update for a Null attribute with arithmetic expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and NULL type', function() { + // Case: Update for a Null attribute with string expression + + var values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Boolean type', function() { + // Case: Update for a Boolean attribute without expression + + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin9.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with arithmetic expression + + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin10.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with string expression + var values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/ws1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin9.json')) + .query({type: 'WeatherStation'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON document attribute without expression + var values = [ + { + name: 'm', + type: 'Object', + value: { name: 'Manufacturer1', VAT: 'U12345678' } + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin6.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON array attribute without expression + + var values = [ + { + name: 'r', + type: 'Object', + value: ['v0.1', 'v0.2', 'v0.3'] + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin7.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are not updated', function() { + + var values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin12.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are updated', function() { + + var values = [ + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin13.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are updated' + + '(overriding situation)', function() { + + var values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + }, + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/entities/light1/attrs', utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin13.json')) + .query({type: 'Light'}) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + +});