Browse Source

MVEL executor implementation

pull/7455/head
Andrii Shvaika 4 years ago
parent
commit
d3f06bd3a4
  1. 9
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 30
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  3. 6
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  4. 176
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  5. 170
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java
  6. 133
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java
  7. 6
      application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java
  8. 2
      application/src/test/java/org/thingsboard/server/service/script/ScriptInvokeServiceTest.java
  9. 20
      common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java
  10. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
  11. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
  12. 7
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  13. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptInvokeService.java
  14. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java
  15. 19
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java
  16. 15
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java
  17. 21
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java
  18. 65
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbMapCreator.java
  19. 120
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbReflectiveAccessorOptimizer.java
  20. 16
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  21. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  22. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  23. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  24. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  25. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  26. 21
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  27. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  28. 20
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  29. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java

9
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -32,6 +32,8 @@ import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -90,7 +92,6 @@ import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRpcService; import org.thingsboard.server.service.rpc.TbRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.server.service.session.DeviceSessionCacheService; import org.thingsboard.server.service.session.DeviceSessionCacheService;
import org.thingsboard.server.service.sms.SmsExecutorService; import org.thingsboard.server.service.sms.SmsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;
@ -267,7 +268,11 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private ScriptInvokeService jsSandbox; private JsInvokeService jsInvokeService;
@Autowired(required = false)
@Getter
private MvelInvokeService mvelInvokeService;
@Autowired @Autowired
@Getter @Getter

30
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -86,6 +87,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
@ -443,7 +445,15 @@ class DefaultTbContext implements TbContext {
@Override @Override
public ScriptEngine createJsScriptEngine(String script, String... argNames) { public ScriptEngine createJsScriptEngine(String script, String... argNames) {
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames);
}
@Override
public ScriptEngine createMvelScriptEngine(String script, String... argNames) {
if (mainCtx.getMvelInvokeService() == null) {
throw new RuntimeException("MVEL execution is disabled!");
}
return new RuleNodeMvelScriptEngine(getTenantId(), mainCtx.getMvelInvokeService(), script, argNames);
} }
@Override @Override
@ -698,6 +708,24 @@ class DefaultTbContext implements TbContext {
return mainCtx.getTenantProfileCache().get(getTenantId()); return mainCtx.getTenantProfileCache().get(getTenantId());
} }
@Override
public ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script) {
if (scriptLang == null) {
scriptLang = ScriptLanguage.JS;
}
if (StringUtils.isBlank(script)) {
throw new RuntimeException(scriptLang.name() + " script is blank!");
}
switch (scriptLang) {
case JS:
return createJsScriptEngine(script);
case MVEL:
return createMvelScriptEngine(script, "msg", "metadata", "msgType");
default:
throw new RuntimeException("Unsupported script language: " + scriptLang.name());
}
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString()); metaData.putValue("ruleNodeId", ruleNodeId.toString());

6
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -37,6 +37,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
@ -65,7 +66,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -140,7 +140,7 @@ public class RuleChainController extends BaseController {
private EventService eventService; private EventService eventService;
@Autowired @Autowired
private ScriptInvokeService scriptInvokeService; private JsInvokeService scriptInvokeService;
@Autowired(required = false) @Autowired(required = false)
private ActorSystemContext actorContext; private ActorSystemContext actorContext;
@ -393,7 +393,7 @@ public class RuleChainController extends BaseController {
String errorText = ""; String errorText = "";
ScriptEngine engine = null; ScriptEngine engine = null;
try { try {
engine = new RuleNodeJsScriptEngine(getTenantId(), scriptInvokeService, getCurrentUser().getId(), script, argNames); engine = new RuleNodeJsScriptEngine(getTenantId(), scriptInvokeService, script, argNames);
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) { switch (scriptType) {
case "update": case "update":

176
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -17,17 +17,13 @@ package org.thingsboard.server.service.script;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.RuleNodeScriptFactory; import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -39,86 +35,22 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@Slf4j @Slf4j
public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine { public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService, JsonNode> {
private static final ObjectMapper mapper = new ObjectMapper(); public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) {
private final ScriptInvokeService sandboxService; super(tenantId, scriptInvokeService, script, argNames);
private final UUID scriptId;
private final TenantId tenantId;
private final EntityId entityId;
public RuleNodeJsScriptEngine(TenantId tenantId, ScriptInvokeService sandboxService, EntityId entityId, String script, String... argNames) {
this.tenantId = tenantId;
this.sandboxService = sandboxService;
this.entityId = entityId;
try {
this.scriptId = this.sandboxService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) {
Throwable t = e;
if (e instanceof ExecutionException) {
t = e.getCause();
}
throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t);
}
}
private static String[] prepareArgs(TbMsg msg) {
try {
String[] args = new String[3];
if (msg.getData() != null) {
args[0] = msg.getData();
} else {
args[0] = "";
}
args[1] = mapper.writeValueAsString(msg.getMetaData().getData());
args[2] = msg.getType();
return args;
} catch (Throwable th) {
throw new IllegalArgumentException("Cannot bind js args", th);
}
}
private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
try {
String data = null;
Map<String, String> metadata = null;
String messageType = null;
if (msgData.has(RuleNodeScriptFactory.MSG)) {
JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG);
data = mapper.writeValueAsString(msgPayload);
}
if (msgData.has(RuleNodeScriptFactory.METADATA)) {
JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
});
}
if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
}
String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
} catch (Throwable th) {
throw new RuntimeException("Failed to unbind message data from javascript result", th);
}
} }
@Override @Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg); return executeScriptAsync(msg);
return Futures.transformAsync(result,
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
} }
ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) { @Override
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
if (json.isObject()) { if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()) { } else if (json.isArray()) {
@ -131,13 +63,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
} }
@Override @Override
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) { protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
return Futures.transformAsync(executeScriptAsync(prevMsg),
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
}
ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
if (!result.isObject()) { if (!result.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType()); log.warn("Wrong result type: {}", result.getNodeType());
Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
@ -146,18 +72,12 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
} }
@Override @Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) { protected JsonNode convertResult(Object result) {
return executeScriptAsync(msg); return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
} }
@Override @Override
public ListenableFuture<String> executeToStringAsync(TbMsg msg) { protected ListenableFuture<String> executeToStringTransform(JsonNode result) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeToStringTransform,
MoreExecutors.directExecutor());
}
ListenableFuture<String> executeToStringTransform(JsonNode result) {
if (result.isTextual()) { if (result.isTextual()) {
return Futures.immediateFuture(result.asText()); return Futures.immediateFuture(result.asText());
} }
@ -166,13 +86,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
} }
@Override @Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { protected ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeFilterTransform,
MoreExecutors.directExecutor());
}
ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
if (json.isBoolean()) { if (json.isBoolean()) {
return Futures.immediateFuture(json.asBoolean()); return Futures.immediateFuture(json.asBoolean());
} }
@ -180,7 +94,8 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} }
ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) { @Override
protected ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
if (result.isTextual()) { if (result.isTextual()) {
return Futures.immediateFuture(Collections.singleton(result.asText())); return Futures.immediateFuture(Collections.singleton(result.asText()));
} }
@ -201,36 +116,37 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
} }
@Override @Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) { protected Object[] prepareArgs(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg), String[] args = new String[3];
this::executeSwitchTransform, if (msg.getData() != null) {
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor args[0] = msg.getData();
} } else {
args[0] = "";
ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) { }
log.trace("execute script async, msg {}", msg); args[1] = JacksonUtil.toString(msg.getMetaData().getData());
String[] inArgs = prepareArgs(msg); args[2] = msg.getType();
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); return args;
}
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, String... args) {
return Futures.transformAsync(sandboxService.invokeScript(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());
} else if (e.getCause() instanceof RuntimeException) {
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
} else {
return Futures.immediateFailedFuture(new ScriptException(e));
}
}
}, MoreExecutors.directExecutor());
} }
public void destroy() { private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
sandboxService.release(this.scriptId); String data = null;
Map<String, String> metadata = null;
String messageType = null;
if (msgData.has(RuleNodeScriptFactory.MSG)) {
JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG);
data = JacksonUtil.toString(msgPayload);
}
if (msgData.has(RuleNodeScriptFactory.METADATA)) {
JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() {
});
}
if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
}
String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
} }
} }

170
application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java

@ -0,0 +1,170 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeService, Object> {
public RuleNodeMvelScriptEngine(TenantId tenantId, MvelInvokeService scriptInvokeService, String script, String... argNames) {
super(tenantId, scriptInvokeService, script, argNames);
}
@Override
protected ListenableFuture<Boolean> executeFilterTransform(Object result) {
if (result instanceof Boolean) {
return Futures.immediateFuture((Boolean) result);
}
return wrongResultType(result);
}
@Override
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, Object result) {
if (result instanceof Map) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg((Map) result, msg)));
} else if (result instanceof Collection) {
List<TbMsg> res = new ArrayList<>();
for (Object resObject : (Collection) result) {
if (resObject instanceof Map) {
res.add(unbindMsg((Map) result, msg));
} else {
return wrongResultType(resObject);
}
}
return Futures.immediateFuture(res);
}
return wrongResultType(result);
}
@Override
protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, Object result) {
if (result instanceof Map) {
return Futures.immediateFuture(unbindMsg((Map) result, prevMsg));
}
return wrongResultType(result);
}
@Override
protected ListenableFuture<String> executeToStringTransform(Object result) {
if (result instanceof String) {
return Futures.immediateFuture((String) result);
} else {
return Futures.immediateFuture(JacksonUtil.toString(result));
}
}
@Override
protected ListenableFuture<Set<String>> executeSwitchTransform(Object result) {
if (result instanceof String) {
return Futures.immediateFuture(Collections.singleton((String) result));
} else if (result instanceof Collection) {
Set<String> res = new HashSet<>();
for (Object resObject : (Collection) result) {
if (resObject instanceof String) {
res.add((String) resObject);
} else {
return wrongResultType(resObject);
}
}
return Futures.immediateFuture(res);
}
return wrongResultType(result);
}
@Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, MoreExecutors.directExecutor());
}
@Override
protected Object convertResult(Object result) {
return result;
}
@Override
protected Object[] prepareArgs(TbMsg msg) {
Object[] args = new Object[3];
if (msg.getData() != null) {
args[0] = JacksonUtil.fromString(msg.getData(), Map.class);
} else {
args[0] = new HashMap<>();
}
args[1] = msg.getMetaData().getData();
args[2] = msg.getType();
return args;
}
private static TbMsg unbindMsg(Map msgData, TbMsg msg) {
String data = null;
Map<String, String> metadata = null;
String messageType = null;
if (msgData.containsKey(RuleNodeScriptFactory.MSG)) {
data = JacksonUtil.toString(msgData.get(RuleNodeScriptFactory.MSG));
}
if (msgData.containsKey(RuleNodeScriptFactory.METADATA)) {
Object msgMetadataObj = msgData.get(RuleNodeScriptFactory.METADATA);
if (msgMetadataObj instanceof Map) {
metadata = ((Map<?, ?>) msgMetadataObj).entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()));
} else {
metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {
});
}
}
if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) {
messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).toString();
}
String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
}
private static <T> ListenableFuture<T> wrongResultType(Object result) {
String className = toClassName(result);
log.warn("Wrong result type: {}", className);
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className));
}
private static String toClassName(Object result) {
return result != null ? result.getClass().getSimpleName() : "null";
}
}

133
application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@Slf4j
public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> implements ScriptEngine {
private final T scriptInvokeService;
private final UUID scriptId;
private final TenantId tenantId;
public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) {
this.tenantId = tenantId;
this.scriptInvokeService = scriptInvokeService;
try {
this.scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get();
} catch (Exception e) {
Throwable t = e;
if (e instanceof ExecutionException) {
t = e.getCause();
}
throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t);
}
}
protected abstract Object[] prepareArgs(TbMsg msg);
@Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<R> result = executeScriptAsync(msg);
return Futures.transformAsync(result,
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
}
protected abstract ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, R result);
@Override
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
return Futures.transformAsync(executeScriptAsync(prevMsg),
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
}
protected abstract ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, R result);
@Override
public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor());
}
@Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeFilterTransform,
MoreExecutors.directExecutor());
}
protected abstract ListenableFuture<String> executeToStringTransform(R result);
protected abstract ListenableFuture<Boolean> executeFilterTransform(R result);
protected abstract ListenableFuture<Set<String>> executeSwitchTransform(R result);
@Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeSwitchTransform,
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
}
ListenableFuture<R> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg);
Object[] inArgs = prepareArgs(msg);
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
ListenableFuture<R> executeScriptAsync(CustomerId customerId, Object... args) {
return Futures.transformAsync(scriptInvokeService.invokeScript(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(convertResult(o));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());
} else if (e.getCause() instanceof RuntimeException) {
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
} else {
return Futures.immediateFailedFuture(new ScriptException(e));
}
}
}, MoreExecutors.directExecutor());
}
public void destroy() {
scriptInvokeService.release(this.scriptId);
}
protected abstract R convertResult(Object result);
}

6
application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java

@ -20,8 +20,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -30,7 +30,7 @@ import java.util.UUID;
@Slf4j @Slf4j
@Service @Service
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "mock") @ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "mock")
public class MockJsInvokeService implements ScriptInvokeService { public class MockJsInvokeService implements JsInvokeService {
@Override @Override
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
@ -39,7 +39,7 @@ public class MockJsInvokeService implements ScriptInvokeService {
} }
@Override @Override
public ListenableFuture<String> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) { public ListenableFuture<Object> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
log.warn("invokeFunction {} {} {} {}", tenantId, customerId, scriptId, args); log.warn("invokeFunction {} {} {} {}", tenantId, customerId, scriptId, args);
return Futures.immediateFuture("{}"); return Futures.immediateFuture("{}");
} }

2
application/src/test/java/org/thingsboard/server/service/script/ScriptInvokeServiceTest.java

@ -92,7 +92,7 @@ class ScriptInvokeServiceTest extends AbstractControllerTest {
} }
private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException { private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException {
return jsInvokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get(); return jsInvokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString();
} }
} }

20
common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.script;
public enum ScriptLanguage {
JS, MVEL
}

2
common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java

@ -17,8 +17,8 @@ package org.thingsboard.server.service.script;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.util.JsonFormat; import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.kafka.TbKafkaEncoder; import org.thingsboard.server.queue.kafka.TbKafkaEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;

2
common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java

@ -16,9 +16,9 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.google.protobuf.util.JsonFormat; import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.kafka.TbKafkaDecoder; import org.thingsboard.server.queue.kafka.TbKafkaDecoder;
import java.io.IOException; import java.io.IOException;

7
common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -135,7 +136,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
} }
@Override @Override
public ListenableFuture<String> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) { public ListenableFuture<Object> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) { if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) {
if (!isScriptPresent(scriptId)) { if (!isScriptPresent(scriptId)) {
return error("No compiled script found for scriptId: [" + scriptId + "]!"); return error("No compiled script found for scriptId: [" + scriptId + "]!");
@ -152,13 +153,13 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
pushedMsgs.incrementAndGet(); pushedMsgs.incrementAndGet();
log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout()); log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout());
var resultFuture = Futures.transformAsync(doInvokeFunction(scriptId, args), output -> { var resultFuture = Futures.transformAsync(doInvokeFunction(scriptId, args), output -> {
String result = output.toString(); String result = JacksonUtil.toString(output);
if (resultSizeExceeded(result)) { if (resultSizeExceeded(result)) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException( throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException(
format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()) format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize())
)); ));
} }
return Futures.immediateFuture(result); return Futures.immediateFuture(output);
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
return withTimeoutAndStatsCallback(scriptId, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout()); return withTimeoutAndStatsCallback(scriptId, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout());

2
common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptInvokeService.java

@ -25,7 +25,7 @@ public interface ScriptInvokeService {
ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames); ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<String> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args); ListenableFuture<Object> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId); ListenableFuture<Void> release(UUID scriptId);

2
common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java

@ -35,7 +35,7 @@ import java.util.concurrent.ConcurrentHashMap;
* Created by ashvayka on 26.09.18. * Created by ashvayka on 26.09.18.
*/ */
@Slf4j @Slf4j
public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService { public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService implements JsInvokeService{
protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>(); protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>();

19
common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java

@ -1,6 +1,21 @@
package org.thingsboard.script.api.mvel; /**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.script.api.ScriptInvokeService;
public interface MvelInvokeService extends ScriptInvokeService { public interface JsInvokeService extends ScriptInvokeService {
} }

15
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java

@ -20,13 +20,17 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mvel2.MVEL; import org.mvel2.MVEL;
import org.mvel2.ParserContext; import org.mvel2.ParserContext;
import org.mvel2.optimizers.AccessorOptimizer;
import org.mvel2.optimizers.OptimizerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.script.api.AbstractScriptInvokeService; import org.thingsboard.script.api.AbstractScriptInvokeService;
import org.thingsboard.script.api.ScriptType; import org.thingsboard.script.api.ScriptType;
@ -37,6 +41,7 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -45,9 +50,9 @@ import java.util.concurrent.Executor;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Slf4j @Slf4j
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "enabled", matchIfMissing = true) @ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "true", matchIfMissing = true)
@Service @Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService { public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>(); protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>();
private ParserContext parserContext; private ParserContext parserContext;
@ -91,9 +96,15 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService {
super.printStats(); super.printStats();
} }
@SneakyThrows
@PostConstruct @PostConstruct
public void init() { public void init() {
super.init(); super.init();
Field field = ReflectionUtils.findField(OptimizerFactory.class, "accessorCompilers");
ReflectionUtils.makeAccessible(field);
Map<String, AccessorOptimizer> accessorCompilers = (Map<String, AccessorOptimizer>) field.get(null);
accessorCompilers.put(OptimizerFactory.SAFE_REFLECTIVE, new TbReflectiveAccessorOptimizer());
parserContext = new ParserContext(new TbMvelParserConfiguration()); parserContext = new ParserContext(new TbMvelParserConfiguration());
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(2, "mvel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(2, "mvel-executor"));
} }

21
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java

@ -1,2 +1,21 @@
package org.thingsboard.script.api.mvel;public interface MvelInvokeService { /**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import org.thingsboard.script.api.ScriptInvokeService;
public interface MvelInvokeService extends ScriptInvokeService {
} }

65
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbMapCreator.java

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import org.mvel2.compiler.Accessor;
import org.mvel2.compiler.ExecutableAccessor;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.optimizers.impl.refl.collection.ExprValueAccessor;
import java.util.HashMap;
import java.util.Map;
public class TbMapCreator implements Accessor {
private Accessor[] keys;
private Accessor[] vals;
private int size;
public Object getValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory) {
Map map = new HashMap<>(size * 2);
for (int i = size - 1; i != -1; i--) {
//noinspection unchecked
map.put(getKey(i, ctx, elCtx, variableFactory), vals[i].getValue(ctx, elCtx, variableFactory));
}
return map;
}
private Object getKey(int index, Object ctx, Object elCtx, VariableResolverFactory variableFactory) {
Accessor keyAccessor = keys[index];
if (keyAccessor instanceof ExprValueAccessor) {
ExecutableStatement executableStatement = ((ExprValueAccessor) keyAccessor).stmt;
if (executableStatement instanceof ExecutableAccessor) {
return ((ExecutableAccessor) executableStatement).getNode().getName();
}
}
return keys[index].getValue(ctx, elCtx, variableFactory);
}
public TbMapCreator(Accessor[] keys, Accessor[] vals) {
this.size = (this.keys = keys).length;
this.vals = vals;
}
public Object setValue(Object ctx, Object elCtx, VariableResolverFactory variableFactory, Object value) {
// not implemented
return null;
}
public Class getKnownEgressType() {
return Map.class;
}
}

120
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbReflectiveAccessorOptimizer.java

@ -0,0 +1,120 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import org.mvel2.ParserContext;
import org.mvel2.compiler.Accessor;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.optimizers.impl.refl.ReflectiveAccessorOptimizer;
import org.mvel2.optimizers.impl.refl.collection.ArrayCreator;
import org.mvel2.optimizers.impl.refl.collection.ExprValueAccessor;
import org.mvel2.optimizers.impl.refl.collection.ListCreator;
import org.mvel2.optimizers.impl.refl.nodes.Union;
import java.util.List;
import java.util.Map;
import static org.mvel2.util.CompilerTools.expectType;
import static org.mvel2.util.ParseTools.findClass;
import static org.mvel2.util.ParseTools.getBaseComponentType;
import static org.mvel2.util.ParseTools.getSubComponentType;
import static org.mvel2.util.ParseTools.repeatChar;
public class TbReflectiveAccessorOptimizer extends ReflectiveAccessorOptimizer {
private Object ctx;
private Class returnType;
private VariableResolverFactory variableFactory;
@Override
public Accessor optimizeCollection(ParserContext pCtx, Object o, Class type, char[] property, int start, int offset,
Object ctx, Object thisRef, VariableResolverFactory factory) {
this.start = this.cursor = start;
this.length = start + offset;
this.returnType = type;
this.ctx = ctx;
this.variableFactory = factory;
this.pCtx = pCtx;
Accessor root = _getAccessor(o, returnType);
if (property != null && length > start) {
return new Union(pCtx, root, property, cursor, offset);
} else {
return root;
}
}
private Accessor _getAccessor(Object o, Class type) {
if (o instanceof List) {
Accessor[] a = new Accessor[((List) o).size()];
int i = 0;
for (Object item : (List) o) {
a[i++] = _getAccessor(item, type);
}
returnType = List.class;
return new ListCreator(a);
} else if (o instanceof Map) {
Accessor[] k = new Accessor[((Map) o).size()];
Accessor[] v = new Accessor[k.length];
int i = 0;
for (Object item : ((Map) o).keySet()) {
k[i] = _getAccessor(item, type); // key
v[i++] = _getAccessor(((Map) o).get(item), type); // value
}
returnType = Map.class;
return new TbMapCreator(k, v);
} else if (o instanceof Object[]) {
Accessor[] a = new Accessor[((Object[]) o).length];
int i = 0;
int dim = 0;
if (type != null) {
String nm = type.getName();
while (nm.charAt(dim) == '[') dim++;
} else {
type = Object[].class;
dim = 1;
}
try {
Class base = getBaseComponentType(type);
Class cls = dim > 1 ? findClass(null, repeatChar('[', dim - 1) + "L" + base.getName() + ";", pCtx)
: type;
for (Object item : (Object[]) o) {
expectType(pCtx, a[i++] = _getAccessor(item, cls), base, true);
}
return new ArrayCreator(a, getSubComponentType(type));
} catch (ClassNotFoundException e) {
throw new RuntimeException("this error should never throw:" + getBaseComponentType(type).getName(), e);
}
} else {
if (returnType == null) returnType = Object.class;
if (type.isArray()) {
return new ExprValueAccessor((String) o, type, ctx, variableFactory, pCtx);
} else {
return new ExprValueAccessor((String) o, Object.class, ctx, variableFactory, pCtx);
}
}
}
}

16
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -116,6 +116,14 @@ public class JacksonUtil {
} }
} }
public static <T> T treeToValue(JsonNode node, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(node, clazz);
} catch (IOException e) {
throw new IllegalArgumentException("Can't convert value: " + node.toString(), e);
}
}
public static JsonNode toJsonNode(String value) { public static JsonNode toJsonNode(String value) {
if (value == null || value.isEmpty()) { if (value == null || value.isEmpty()) {
return null; return null;
@ -127,14 +135,6 @@ public class JacksonUtil {
} }
} }
public static <T> T treeToValue(JsonNode node, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(node, clazz);
} catch (IOException e) {
throw new IllegalArgumentException("Can't convert value: " + node.toString(), e);
}
}
public static ObjectNode newObjectNode() { public static ObjectNode newObjectNode() {
return OBJECT_MAPPER.createObjectNode(); return OBJECT_MAPPER.createObjectNode();
} }

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -263,6 +264,8 @@ public interface TbContext {
ScriptEngine createJsScriptEngine(String script, String... argNames); ScriptEngine createJsScriptEngine(String script, String... argNames);
ScriptEngine createMvelScriptEngine(String script, String... argNames);
void logJsEvalRequest(); void logJsEvalRequest();
void logJsEvalResponse(); void logJsEvalResponse();
@ -298,4 +301,6 @@ public interface TbContext {
void removeListeners(); void removeListeners();
TenantProfile getTenantProfile(); TenantProfile getTenantProfile();
ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String s);
} }

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java

@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -41,12 +42,13 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
private final ObjectMapper mapper = new ObjectMapper(); private final ObjectMapper mapper = new ObjectMapper();
protected C config; protected C config;
private ScriptEngine buildDetailsJsEngine; private ScriptEngine scriptEngine;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadAlarmNodeConfig(configuration); this.config = loadAlarmNodeConfig(configuration);
this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs()); scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getAlarmDetailsBuildMvel() : config.getAlarmDetailsBuildJs());
} }
protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
@ -80,7 +82,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails)); metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData()); dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData());
} }
return buildDetailsJsEngine.executeJsonAsync(dummyMsg); return scriptEngine.executeJsonAsync(dummyMsg);
} catch (Exception e) { } catch (Exception e) {
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
@ -102,8 +104,8 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
@Override @Override
public void destroy() { public void destroy() {
if (buildDetailsJsEngine != null) { if (scriptEngine != null) {
buildDetailsJsEngine.destroy(); scriptEngine.destroy();
} }
} }

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.action; package org.thingsboard.rule.engine.action;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data @Data
public abstract class TbAbstractAlarmNodeConfiguration { public abstract class TbAbstractAlarmNodeConfiguration {
@ -32,7 +33,22 @@ public abstract class TbAbstractAlarmNodeConfiguration {
"\n" + "\n" +
"return details;"; "return details;";
static final String ALARM_DETAILS_BUILD_MVEL_TEMPLATE = "" +
"var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
" //remove prevAlarmDetails from metadata\n" +
" delete metadata.prevAlarmDetails;\n" +
" //now metadata is the same as it comes IN this rule node\n" +
"}\n" +
"\n" +
"\n" +
"return details;";
private String alarmType; private String alarmType;
private ScriptLanguage scriptLang;
private String alarmDetailsBuildJs; private String alarmDetailsBuildJs;
private String alarmDetailsBuildMvel;
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.action;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data @Data
public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbClearAlarmNodeConfiguration> { public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbClearAlarmNodeConfiguration> {
@ -25,7 +26,9 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat
@Override @Override
public TbClearAlarmNodeConfiguration defaultConfiguration() { public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration(); TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmType("General Alarm"); configuration.setAlarmType("General Alarm");
return configuration; return configuration;
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.action;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -38,7 +39,9 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
@Override @Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() { public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration(); TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmType("General Alarm"); configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL.name()); configuration.setSeverity(AlarmSeverity.CRITICAL.name());
configuration.setPropagate(false); configuration.setPropagate(false);

21
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -23,7 +23,9 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback; import static org.thingsboard.common.util.DonAsynchron.withCallback;
@ -38,25 +40,26 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
"Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" + "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
"Message type can be accessed via <code>msgType</code> property.", "Message type can be accessed via <code>msgType</code> property."
uiResources = {"static/rulenode/rulenode-core-config.js"}, // uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeScriptConfig") // configDirective = "tbFilterNodeScriptConfig")
)
public class TbJsFilterNode implements TbNode { public class TbJsFilterNode implements TbNode {
private TbJsFilterNodeConfiguration config; private TbJsFilterNodeConfiguration config;
private ScriptEngine jsEngine; private ScriptEngine scriptEngine;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript());
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
ctx.logJsEvalRequest(); ctx.logJsEvalRequest();
withCallback(jsEngine.executeFilterAsync(msg), withCallback(scriptEngine.executeFilterAsync(msg),
filterResult -> { filterResult -> {
ctx.logJsEvalResponse(); ctx.logJsEvalResponse();
ctx.tellNext(msg, filterResult ? "True" : "False"); ctx.tellNext(msg, filterResult ? "True" : "False");
@ -69,8 +72,8 @@ public class TbJsFilterNode implements TbNode {
@Override @Override
public void destroy() { public void destroy() {
if (jsEngine != null) { if (scriptEngine != null) {
jsEngine.destroy(); scriptEngine.destroy();
} }
} }
} }

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -17,16 +17,21 @@ package org.thingsboard.rule.engine.filter;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data @Data
public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> { public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript;
@Override @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setJsScript("return msg.temperature > 20;"); configuration.setJsScript("return msg.temperature > 20;");
configuration.setMvelScript("return msg.temperature > 20;");
return configuration; return configuration;
} }
} }

20
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -22,7 +22,9 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.util.List; import java.util.List;
@ -38,25 +40,27 @@ import java.util.List;
"<code>msgType</code> - is a Message type.<br/>" + "<code>msgType</code> - is a Message type.<br/>" +
"Should return the following structure:<br/>" + "Should return the following structure:<br/>" +
"<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" + "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
"All fields in resulting object are optional and will be taken from original message if not specified.", "All fields in resulting object are optional and will be taken from original message if not specified."
uiResources = {"static/rulenode/rulenode-core-config.js"}, // uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeScriptConfig") // configDirective = "tbTransformationNodeScriptConfig"
)
public class TbTransformMsgNode extends TbAbstractTransformNode { public class TbTransformMsgNode extends TbAbstractTransformNode {
private TbTransformMsgNodeConfiguration config; private TbTransformMsgNodeConfiguration config;
private ScriptEngine jsEngine; private ScriptEngine scriptEngine;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript());
setConfig(config); setConfig(config);
} }
@Override @Override
protected ListenableFuture<List<TbMsg>> transform(TbContext ctx, TbMsg msg) { protected ListenableFuture<List<TbMsg>> transform(TbContext ctx, TbMsg msg) {
ctx.logJsEvalRequest(); ctx.logJsEvalRequest();
return jsEngine.executeUpdateAsync(msg); return scriptEngine.executeUpdateAsync(msg);
} }
@Override @Override
@ -73,8 +77,8 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
@Override @Override
public void destroy() { public void destroy() {
if (jsEngine != null) { if (scriptEngine != null) {
jsEngine.destroy(); scriptEngine.destroy();
} }
} }
} }

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java

@ -17,16 +17,21 @@ package org.thingsboard.rule.engine.transform;
import lombok.Data; import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data @Data
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration { public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
private ScriptLanguage scriptLang;
private String jsScript; private String jsScript;
private String mvelScript;
@Override @Override
public TbTransformMsgNodeConfiguration defaultConfiguration() { public TbTransformMsgNodeConfiguration defaultConfiguration() {
TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration(); TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};"); configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
configuration.setMvelScript("return {msg: msg, metadata: metadata, msgType: msgType};");
return configuration; return configuration;
} }
} }

Loading…
Cancel
Save