diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index f8199caedd..85b8e21095 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.script.api.js.JsInvokeService; +import org.thingsboard.script.api.mvel.MvelInvokeService; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.common.data.EventInfo; @@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -67,6 +69,7 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; +import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -117,10 +120,10 @@ public class RuleChainController extends BaseController { private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " + "List of rule nodes and their connection is stored in a separate 'metadata' object."; private static final String RULE_CHAIN_METADATA_DESCRIPTION = "The metadata object contains information about the rule nodes and their connections."; - private static final String TEST_JS_FUNCTION = "Execute the JavaScript function and return the result. The format of request: \n\n" + private static final String TEST_SCRIPT_FUNCTION = "Execute the Script function and return the result. The format of request: \n\n" + MARKDOWN_CODE_BLOCK_START + "{\n" + - " \"script\": \"Your JS Function as String\",\n" + + " \"script\": \"Your Function as String\",\n" + " \"scriptType\": \"One of: update, generate, filter, switch, json, string\",\n" + " \"argNames\": [\"msg\", \"metadata\", \"type\"],\n" + " \"msg\": \"{\\\"temperature\\\": 42}\", \n" + @@ -140,7 +143,10 @@ public class RuleChainController extends BaseController { private EventService eventService; @Autowired - private JsInvokeService scriptInvokeService; + private JsInvokeService jsInvokeService; + + @Autowired(required = false) + private MvelInvokeService mvelInvokeService; @Autowired(required = false) private ActorSystemContext actorContext; @@ -148,6 +154,9 @@ public class RuleChainController extends BaseController { @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}") private boolean debugPerTenantEnabled; + @Value("${mvel.enabled:true}") + private boolean mvelEnabled; + @ApiOperation(value = "Get Rule Chain (getRuleChainById)", notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @@ -369,13 +378,23 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Is MVEL script executor enabled", + notes = "Returns 'True' if the MVEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/mvelEnabled", method = RequestMethod.GET) + @ResponseBody + public Boolean isMvelEnabled() { + return mvelEnabled; + } - @ApiOperation(value = "Test JavaScript function", - notes = TEST_JS_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) + @ApiOperation(value = "Test Script function", + notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @ResponseBody public JsonNode testScript( + @ApiParam(value = "Script language: JS or MVEL") + @RequestParam(required = false) ScriptLanguage scriptLang, @ApiParam(value = "Test JS request. See API call description above.") @RequestBody JsonNode inputParams) throws ThingsboardException { try { @@ -393,7 +412,17 @@ public class RuleChainController extends BaseController { String errorText = ""; ScriptEngine engine = null; try { - engine = new RuleNodeJsScriptEngine(getTenantId(), scriptInvokeService, script, argNames); + if (scriptLang == null) { + scriptLang = ScriptLanguage.JS; + } + if (ScriptLanguage.JS.equals(scriptLang)) { + engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames); + } else { + if (mvelInvokeService == null) { + throw new IllegalArgumentException("MVEL script engine is disabled!"); + } + engine = new RuleNodeMvelScriptEngine(getTenantId(), mvelInvokeService, script, argNames); + } TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); switch (scriptType) { case "update": diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 373c685cc1..c7d964e017 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -40,9 +40,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "If True - send Message via True chain, otherwise False chain is used." + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + - "Message type can be accessed via msgType property." -// uiResources = {"static/rulenode/rulenode-core-config.js"}, -// configDirective = "tbFilterNodeScriptConfig") + "Message type can be accessed via msgType property.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFilterNodeScriptConfig" ) public class TbJsFilterNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 3ef6f787d4..fd33bb4554 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -40,9 +40,9 @@ import java.util.List; "msgType - is a Message type.
" + "Should return the following structure:
" + "{ msg: new payload,
   metadata: new metadata,
   msgType: new msgType }

" + - "All fields in resulting object are optional and will be taken from original message if not specified." -// uiResources = {"static/rulenode/rulenode-core-config.js"}, -// configDirective = "tbTransformationNodeScriptConfig" + "All fields in resulting object are optional and will be taken from original message if not specified.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbTransformationNodeScriptConfig" ) public class TbTransformMsgNode extends TbAbstractTransformNode { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index d9706f6b96..b4b4563b4d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -180,7 +181,7 @@ public class TbAlarmNodeTest { verifyError(msg, "message", NotImplementedException.class); - verify(ctx).createJsScriptEngine("DETAILS"); + verify(ctx).createScriptEngine(ScriptLanguage.JS, "DETAILS"); verify(ctx).getAlarmService(); verify(ctx, times(3)).getDbCallbackExecutor(); verify(ctx).logJsEvalRequest(); @@ -395,12 +396,13 @@ public class TbAlarmNodeTest { config.setPropagate(true); config.setSeverity("$[alarmSeverity]"); config.setAlarmType("SomeType"); + config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); config.setDynamicSeverity(true); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); @@ -456,6 +458,7 @@ public class TbAlarmNodeTest { public void testCreateAlarmWithDynamicSeverityFromMetadata() throws Exception { TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); config.setPropagate(true); + config.setScriptLang(ScriptLanguage.JS); config.setSeverity("${alarmSeverity}"); config.setAlarmType("SomeType"); config.setAlarmDetailsBuildJs("DETAILS"); @@ -463,7 +466,7 @@ public class TbAlarmNodeTest { ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); @@ -521,12 +524,13 @@ public class TbAlarmNodeTest { config.setPropagateToTenant(true); config.setSeverity(CRITICAL.name()); config.setAlarmType("SomeType" + i); + config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); config.setDynamicSeverity(true); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); @@ -584,11 +588,12 @@ public class TbAlarmNodeTest { config.setPropagate(true); config.setSeverity(CRITICAL.name()); config.setAlarmType("SomeType"); + config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); @@ -605,11 +610,12 @@ public class TbAlarmNodeTest { try { TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration(); config.setAlarmType("SomeType"); + config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); + when(ctx.createScriptEngine(ScriptLanguage.JS, "DETAILS")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getAlarmService()).thenReturn(alarmService); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index 63c94ea9a5..f40c6f0072 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -54,8 +55,6 @@ public class TbJsFilterNodeTest { @Mock private TbContext ctx; @Mock - private ListeningExecutor executor; - @Mock private ScriptEngine scriptEngine; private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); @@ -73,7 +72,7 @@ public class TbJsFilterNodeTest { } @Test - public void exceptionInJsThrowsException() throws TbNodeException, ScriptException { + public void exceptionInJsThrowsException() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); @@ -85,7 +84,7 @@ public class TbJsFilterNodeTest { } @Test - public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException { + public void metadataConditionCanBeTrue() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); @@ -98,11 +97,12 @@ public class TbJsFilterNodeTest { private void initWithScript() throws TbNodeException { TbJsFilterNodeConfiguration config = new TbJsFilterNodeConfiguration(); + config.setScriptLang(ScriptLanguage.JS); config.setJsScript("scr"); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine); + when(ctx.createScriptEngine(ScriptLanguage.JS, "scr")).thenReturn(scriptEngine); node = new TbJsFilterNode(); node.init(ctx, nodeConfiguration); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index 411db21c7f..89480b404e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -55,12 +56,10 @@ public class TbTransformMsgNodeTest { @Mock private TbContext ctx; @Mock - private ListeningExecutor executor; - @Mock private ScriptEngine scriptEngine; @Test - public void metadataCanBeUpdated() throws TbNodeException, ScriptException { + public void metadataCanBeUpdated() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); @@ -80,7 +79,7 @@ public class TbTransformMsgNodeTest { } @Test - public void exceptionHandledCorrectly() throws TbNodeException, ScriptException { + public void exceptionHandledCorrectly() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); @@ -97,11 +96,12 @@ public class TbTransformMsgNodeTest { private void initWithScript() throws TbNodeException { TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration(); + config.setScriptLang(ScriptLanguage.JS); config.setJsScript("scr"); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine); + when(ctx.createScriptEngine(ScriptLanguage.JS, "scr")).thenReturn(scriptEngine); node = new TbTransformMsgNode(); node.init(ctx, nodeConfiguration);