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.SmsService;
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.tenant.DebugTbRateLimits;
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.TbRpcService;
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.sms.SmsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService;
@ -267,7 +268,11 @@ public class ActorSystemContext {
@Autowired
@Getter
private ScriptInvokeService jsSandbox;
private JsInvokeService jsInvokeService;
@Autowired(required = false)
@Getter
private MvelInvokeService mvelInvokeService;
@Autowired
@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.rule.RuleNode;
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.TbMsg;
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.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import java.util.Collections;
import java.util.Set;
@ -443,7 +445,15 @@ class DefaultTbContext implements TbContext {
@Override
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
@ -698,6 +708,24 @@ class DefaultTbContext implements TbContext {
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) {
TbMsgMetaData metaData = new TbMsgMetaData();
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.RestController;
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.tenant.DebugTbRateLimits;
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.queue.util.TbCoreComponent;
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.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -140,7 +140,7 @@ public class RuleChainController extends BaseController {
private EventService eventService;
@Autowired
private ScriptInvokeService scriptInvokeService;
private JsInvokeService scriptInvokeService;
@Autowired(required = false)
private ActorSystemContext actorContext;
@ -393,7 +393,7 @@ public class RuleChainController extends BaseController {
String errorText = "";
ScriptEngine engine = null;
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);
switch (scriptType) {
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.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.script.api.ScriptInvokeService;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.common.util.JacksonUtil;
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.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -39,86 +35,22 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@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();
private final ScriptInvokeService sandboxService;
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);
}
public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) {
super(tenantId, scriptInvokeService, script, argNames);
}
@Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result,
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return executeScriptAsync(msg);
}
ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
@Override
protected ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()) {
@ -131,13 +63,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
}
@Override
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
return Futures.transformAsync(executeScriptAsync(prevMsg),
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
}
ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
protected ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
if (!result.isObject()) {
log.warn("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
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return executeScriptAsync(msg);
protected JsonNode convertResult(Object result) {
return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
}
@Override
public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeToStringTransform,
MoreExecutors.directExecutor());
}
ListenableFuture<String> executeToStringTransform(JsonNode result) {
protected ListenableFuture<String> executeToStringTransform(JsonNode result) {
if (result.isTextual()) {
return Futures.immediateFuture(result.asText());
}
@ -166,13 +86,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
}
@Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeFilterTransform,
MoreExecutors.directExecutor());
}
ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
protected ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
if (json.isBoolean()) {
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()));
}
ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
@Override
protected ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
if (result.isTextual()) {
return Futures.immediateFuture(Collections.singleton(result.asText()));
}
@ -201,36 +116,37 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
}
@Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeSwitchTransform,
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
}
ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg);
String[] inArgs = prepareArgs(msg);
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
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());
protected Object[] prepareArgs(TbMsg msg) {
String[] args = new String[3];
if (msg.getData() != null) {
args[0] = msg.getData();
} else {
args[0] = "";
}
args[1] = JacksonUtil.toString(msg.getMetaData().getData());
args[2] = msg.getType();
return args;
}
public void destroy() {
sandboxService.release(this.scriptId);
private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
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 org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.script.api.ScriptInvokeService;
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.TenantId;
@ -30,7 +30,7 @@ import java.util.UUID;
@Slf4j
@Service
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "mock")
public class MockJsInvokeService implements ScriptInvokeService {
public class MockJsInvokeService implements JsInvokeService {
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
@ -39,7 +39,7 @@ public class MockJsInvokeService implements ScriptInvokeService {
}
@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);
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 {
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.util.JsonFormat;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.kafka.TbKafkaEncoder;
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;
import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.kafka.TbKafkaDecoder;
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 lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId;
@ -135,7 +136,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
}
@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 (!isScriptPresent(scriptId)) {
return error("No compiled script found for scriptId: [" + scriptId + "]!");
@ -152,13 +153,13 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService
pushedMsgs.incrementAndGet();
log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout());
var resultFuture = Futures.transformAsync(doInvokeFunction(scriptId, args), output -> {
String result = output.toString();
String result = JacksonUtil.toString(output);
if (resultSizeExceeded(result)) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException(
format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize())
));
}
return Futures.immediateFuture(result);
return Futures.immediateFuture(output);
}, MoreExecutors.directExecutor());
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<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);

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.
*/
@Slf4j
public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService {
public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService implements JsInvokeService{
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;
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.MoreExecutors;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.optimizers.AccessorOptimizer;
import org.mvel2.optimizers.OptimizerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.script.api.AbstractScriptInvokeService;
import org.thingsboard.script.api.ScriptType;
@ -37,6 +41,7 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@ -45,9 +50,9 @@ import java.util.concurrent.Executor;
import java.util.regex.Pattern;
@Slf4j
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "enabled", matchIfMissing = true)
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "true", matchIfMissing = true)
@Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService {
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>();
private ParserContext parserContext;
@ -91,9 +96,15 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService {
super.printStats();
}
@SneakyThrows
@PostConstruct
public void 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());
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) {
if (value == null || value.isEmpty()) {
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() {
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.rule.RuleNode;
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.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
@ -263,6 +264,8 @@ public interface TbContext {
ScriptEngine createJsScriptEngine(String script, String... argNames);
ScriptEngine createMvelScriptEngine(String script, String... argNames);
void logJsEvalRequest();
void logJsEvalResponse();
@ -298,4 +301,6 @@ public interface TbContext {
void removeListeners();
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.TbNodeException;
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.TbMsgMetaData;
import org.thingsboard.common.util.JacksonUtil;
@ -41,12 +42,13 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
private final ObjectMapper mapper = new ObjectMapper();
protected C config;
private ScriptEngine buildDetailsJsEngine;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
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;
@ -80,7 +82,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData());
}
return buildDetailsJsEngine.executeJsonAsync(dummyMsg);
return scriptEngine.executeJsonAsync(dummyMsg);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
@ -102,8 +104,8 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
@Override
public void destroy() {
if (buildDetailsJsEngine != null) {
buildDetailsJsEngine.destroy();
if (scriptEngine != null) {
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;
import lombok.Data;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
public abstract class TbAbstractAlarmNodeConfiguration {
@ -32,7 +33,22 @@ public abstract class TbAbstractAlarmNodeConfiguration {
"\n" +
"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 ScriptLanguage scriptLang;
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 org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbClearAlarmNodeConfiguration> {
@ -25,7 +26,9 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat
@Override
public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmType("General Alarm");
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 org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import java.util.Collections;
import java.util.List;
@ -38,7 +39,9 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
@Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL.name());
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.TbNodeException;
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.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg;
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." +
"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 type can be accessed via <code>msgType</code> property.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbFilterNodeScriptConfig")
"Message type can be accessed via <code>msgType</code> property."
// uiResources = {"static/rulenode/rulenode-core-config.js"},
// configDirective = "tbFilterNodeScriptConfig")
)
public class TbJsFilterNode implements TbNode {
private TbJsFilterNodeConfiguration config;
private ScriptEngine jsEngine;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
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
public void onMsg(TbContext ctx, TbMsg msg) {
ctx.logJsEvalRequest();
withCallback(jsEngine.executeFilterAsync(msg),
withCallback(scriptEngine.executeFilterAsync(msg),
filterResult -> {
ctx.logJsEvalResponse();
ctx.tellNext(msg, filterResult ? "True" : "False");
@ -69,8 +72,8 @@ public class TbJsFilterNode implements TbNode {
@Override
public void destroy() {
if (jsEngine != null) {
jsEngine.destroy();
if (scriptEngine != null) {
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 org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
@Override
public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setJsScript("return msg.temperature > 20;");
configuration.setMvelScript("return msg.temperature > 20;");
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.TbNodeException;
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.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
@ -38,25 +40,27 @@ import java.util.List;
"<code>msgType</code> - is a Message type.<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/>" +
"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 {
private TbTransformMsgNodeConfiguration config;
private ScriptEngine jsEngine;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
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);
}
@Override
protected ListenableFuture<List<TbMsg>> transform(TbContext ctx, TbMsg msg) {
ctx.logJsEvalRequest();
return jsEngine.executeUpdateAsync(msg);
return scriptEngine.executeUpdateAsync(msg);
}
@Override
@ -73,8 +77,8 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
@Override
public void destroy() {
if (jsEngine != null) {
jsEngine.destroy();
if (scriptEngine != null) {
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 org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
@Override
public TbTransformMsgNodeConfiguration defaultConfiguration() {
TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
configuration.setMvelScript("return {msg: msg, metadata: metadata, msgType: msgType};");
return configuration;
}
}

Loading…
Cancel
Save