commit
						d494a8883a
					
				@ -17,18 +17,16 @@ 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 lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.script.api.RuleNodeScriptFactory;
 | 
			
		||||
import org.thingsboard.script.api.TbScriptException;
 | 
			
		||||
import org.thingsboard.script.api.js.JsInvokeService;
 | 
			
		||||
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.Collections;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
@ -36,85 +34,12 @@ import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService, JsonNode> {
 | 
			
		||||
 | 
			
		||||
    public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) {
 | 
			
		||||
        super(tenantId, scriptInvokeService, script, argNames);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
 | 
			
		||||
        return executeScriptAsync(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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()) {
 | 
			
		||||
            List<TbMsg> res = new ArrayList<>(json.size());
 | 
			
		||||
            json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
 | 
			
		||||
            return Futures.immediateFuture(res);
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("Wrong result type: {}", json.getNodeType());
 | 
			
		||||
        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    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()));
 | 
			
		||||
        }
 | 
			
		||||
        return Futures.immediateFuture(unbindMsg(result, prevMsg));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected JsonNode convertResult(Object result) {
 | 
			
		||||
        return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<String> executeToStringTransform(JsonNode result) {
 | 
			
		||||
        if (result.isTextual()) {
 | 
			
		||||
            return Futures.immediateFuture(result.asText());
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("Wrong result type: {}", result.getNodeType());
 | 
			
		||||
        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
 | 
			
		||||
        if (json.isBoolean()) {
 | 
			
		||||
            return Futures.immediateFuture(json.asBoolean());
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("Wrong result type: {}", json.getNodeType());
 | 
			
		||||
        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
 | 
			
		||||
        if (result.isTextual()) {
 | 
			
		||||
            return Futures.immediateFuture(Collections.singleton(result.asText()));
 | 
			
		||||
        }
 | 
			
		||||
        if (result.isArray()) {
 | 
			
		||||
            Set<String> nextStates = new HashSet<>();
 | 
			
		||||
            for (JsonNode val : result) {
 | 
			
		||||
                if (!val.isTextual()) {
 | 
			
		||||
                    log.warn("Wrong result type: {}", val.getNodeType());
 | 
			
		||||
                    return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType()));
 | 
			
		||||
                } else {
 | 
			
		||||
                    nextStates.add(val.asText());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return Futures.immediateFuture(nextStates);
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("Wrong result type: {}", result.getNodeType());
 | 
			
		||||
        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Object[] prepareArgs(TbMsg msg) {
 | 
			
		||||
        String[] args = new String[3];
 | 
			
		||||
@ -128,6 +53,71 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService
 | 
			
		||||
        return args;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected List<TbMsg> executeUpdateTransform(TbMsg msg, JsonNode json) {
 | 
			
		||||
        if (json.isObject()) {
 | 
			
		||||
            return Collections.singletonList(unbindMsg(json, msg));
 | 
			
		||||
        } else if (json.isArray()) {
 | 
			
		||||
            List<TbMsg> res = new ArrayList<>(json.size());
 | 
			
		||||
            json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbMsg executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
 | 
			
		||||
        if (!result.isObject()) {
 | 
			
		||||
            throw wrongResultType(result);
 | 
			
		||||
        }
 | 
			
		||||
        return unbindMsg(result, prevMsg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean executeFilterTransform(JsonNode json) {
 | 
			
		||||
        if (json.isBoolean()) {
 | 
			
		||||
            return json.asBoolean();
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Set<String> executeSwitchTransform(JsonNode result) {
 | 
			
		||||
        if (result.isTextual()) {
 | 
			
		||||
            return Collections.singleton(result.asText());
 | 
			
		||||
        }
 | 
			
		||||
        if (result.isArray()) {
 | 
			
		||||
            Set<String> nextStates = new HashSet<>();
 | 
			
		||||
            for (JsonNode val : result) {
 | 
			
		||||
                if (!val.isTextual()) {
 | 
			
		||||
                    throw wrongResultType(val);
 | 
			
		||||
                } else {
 | 
			
		||||
                    nextStates.add(val.asText());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return nextStates;
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
 | 
			
		||||
        return executeScriptAsync(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String executeToStringTransform(JsonNode result) {
 | 
			
		||||
        if (result.isTextual()) {
 | 
			
		||||
            return result.asText();
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected JsonNode convertResult(Object result) {
 | 
			
		||||
        return JacksonUtil.toJsonNode(result != null ? result.toString() : null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
 | 
			
		||||
        String data = null;
 | 
			
		||||
        Map<String, String> metadata = null;
 | 
			
		||||
@ -138,19 +128,23 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine<JsInvokeService
 | 
			
		||||
        }
 | 
			
		||||
        if (msgData.has(RuleNodeScriptFactory.METADATA)) {
 | 
			
		||||
            JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
 | 
			
		||||
            metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() {
 | 
			
		||||
            });
 | 
			
		||||
            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();
 | 
			
		||||
        String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType();
 | 
			
		||||
        return msg.transform()
 | 
			
		||||
                .type(newMessageType)
 | 
			
		||||
                .metaData(newMetadata)
 | 
			
		||||
                .data(newData)
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private TbScriptException wrongResultType(JsonNode result) {
 | 
			
		||||
        return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + result.getNodeType()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,41 +17,44 @@ 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.script.api.TbScriptException;
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> implements ScriptEngine {
 | 
			
		||||
 | 
			
		||||
    private final T scriptInvokeService;
 | 
			
		||||
 | 
			
		||||
    private final UUID scriptId;
 | 
			
		||||
    protected 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();
 | 
			
		||||
            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);
 | 
			
		||||
            if (t instanceof TbScriptException scriptException) {
 | 
			
		||||
                throw scriptException;
 | 
			
		||||
            }
 | 
			
		||||
            throw new RuntimeException("Unexpected error when creating script engine: " + t.getMessage(), t);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -60,74 +63,53 @@ public abstract class RuleNodeScriptEngine<T extends ScriptInvokeService, R> imp
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
 | 
			
		||||
        ListenableFuture<R> result = executeScriptAsync(msg);
 | 
			
		||||
        return Futures.transformAsync(result,
 | 
			
		||||
                json -> executeUpdateTransform(msg, json),
 | 
			
		||||
                MoreExecutors.directExecutor());
 | 
			
		||||
        return Futures.transform(result, json -> executeUpdateTransform(msg, json), directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, R result);
 | 
			
		||||
    protected abstract 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());
 | 
			
		||||
        return Futures.transform(executeScriptAsync(prevMsg), result -> executeGenerateTransform(prevMsg, result), directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract TbMsg executeGenerateTransform(TbMsg prevMsg, R result);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
 | 
			
		||||
        return Futures.transformAsync(executeScriptAsync(msg),
 | 
			
		||||
                this::executeFilterTransform,
 | 
			
		||||
                MoreExecutors.directExecutor());
 | 
			
		||||
        return Futures.transform(executeScriptAsync(msg), this::executeFilterTransform, directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract ListenableFuture<String> executeToStringTransform(R result);
 | 
			
		||||
 | 
			
		||||
    protected abstract ListenableFuture<Boolean> executeFilterTransform(R result);
 | 
			
		||||
 | 
			
		||||
    protected abstract ListenableFuture<Set<String>> executeSwitchTransform(R result);
 | 
			
		||||
    protected abstract boolean executeFilterTransform(R result);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
 | 
			
		||||
        return Futures.transformAsync(executeScriptAsync(msg),
 | 
			
		||||
                this::executeSwitchTransform,
 | 
			
		||||
                MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
 | 
			
		||||
        return Futures.transform(executeScriptAsync(msg), this::executeSwitchTransform, directExecutor()); // usually runs on a callbackExecutor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract Set<String> executeSwitchTransform(R result);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
 | 
			
		||||
        return Futures.transform(executeScriptAsync(msg), this::executeToStringTransform, directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract String executeToStringTransform(R result);
 | 
			
		||||
 | 
			
		||||
    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());
 | 
			
		||||
    private ListenableFuture<R> executeScriptAsync(CustomerId customerId, Object... args) {
 | 
			
		||||
        return Futures.transform(scriptInvokeService.invokeScript(tenantId, customerId, scriptId, args), this::convertResult, directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
        scriptInvokeService.release(this.scriptId);
 | 
			
		||||
        scriptInvokeService.release(scriptId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected abstract R convertResult(Object result);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,17 +19,15 @@ 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.TbScriptException;
 | 
			
		||||
import org.thingsboard.script.api.tbel.TbelInvokeService;
 | 
			
		||||
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;
 | 
			
		||||
@ -40,86 +38,14 @@ import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 | 
			
		||||
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> {
 | 
			
		||||
 | 
			
		||||
    public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService 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) resObject, 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];
 | 
			
		||||
@ -133,6 +59,74 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
 | 
			
		||||
        return args;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected List<TbMsg> executeUpdateTransform(TbMsg msg, Object result) {
 | 
			
		||||
        if (result instanceof Map msgData) {
 | 
			
		||||
            return Collections.singletonList(unbindMsg(msgData, msg));
 | 
			
		||||
        } else if (result instanceof Collection resultCollection) {
 | 
			
		||||
            List<TbMsg> res = new ArrayList<>(resultCollection.size());
 | 
			
		||||
            for (Object resObject : resultCollection) {
 | 
			
		||||
                if (resObject instanceof Map msgData) {
 | 
			
		||||
                    res.add(unbindMsg(msgData, msg));
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw wrongResultType(resObject);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected TbMsg executeGenerateTransform(TbMsg prevMsg, Object result) {
 | 
			
		||||
        if (result instanceof Map msgData) {
 | 
			
		||||
            return unbindMsg(msgData, prevMsg);
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean executeFilterTransform(Object result) {
 | 
			
		||||
        if (result instanceof Boolean b) {
 | 
			
		||||
            return b;
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Set<String> executeSwitchTransform(Object result) {
 | 
			
		||||
        if (result instanceof String str) {
 | 
			
		||||
            return Collections.singleton(str);
 | 
			
		||||
        }
 | 
			
		||||
        if (result instanceof Collection<?> resultCollection) {
 | 
			
		||||
            Set<String> res = new HashSet<>(resultCollection.size());
 | 
			
		||||
            for (Object resObject : resultCollection) {
 | 
			
		||||
                if (resObject instanceof String str) {
 | 
			
		||||
                    res.add(str);
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw wrongResultType(resObject);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
        throw wrongResultType(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
 | 
			
		||||
        return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, directExecutor());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected Object convertResult(Object result) {
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String executeToStringTransform(Object result) {
 | 
			
		||||
        return result instanceof String str ? str : JacksonUtil.toString(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static TbMsg unbindMsg(Map msgData, TbMsg msg) {
 | 
			
		||||
        String data = null;
 | 
			
		||||
        Map<String, String> metadata = null;
 | 
			
		||||
@ -142,12 +136,12 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
 | 
			
		||||
        }
 | 
			
		||||
        if (msgData.containsKey(RuleNodeScriptFactory.METADATA)) {
 | 
			
		||||
            Object msgMetadataObj = msgData.get(RuleNodeScriptFactory.METADATA);
 | 
			
		||||
            if (msgMetadataObj instanceof Map) {
 | 
			
		||||
                metadata = ((Map<?, ?>) msgMetadataObj).entrySet().stream().filter(e -> e.getValue() != null)
 | 
			
		||||
            if (msgMetadataObj instanceof Map<?, ?> msgMetadataObjAsMap) {
 | 
			
		||||
                metadata = msgMetadataObjAsMap.entrySet().stream()
 | 
			
		||||
                        .filter(e -> e.getValue() != null)
 | 
			
		||||
                        .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString()));
 | 
			
		||||
            } else {
 | 
			
		||||
                metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {
 | 
			
		||||
                });
 | 
			
		||||
                metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {});
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) {
 | 
			
		||||
@ -155,7 +149,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
 | 
			
		||||
        }
 | 
			
		||||
        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();
 | 
			
		||||
        String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType();
 | 
			
		||||
        return msg.transform()
 | 
			
		||||
                .type(newMessageType)
 | 
			
		||||
                .metaData(newMetadata)
 | 
			
		||||
@ -163,13 +157,13 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeSer
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static <T> ListenableFuture<T> wrongResultType(Object result) {
 | 
			
		||||
    private TbScriptException wrongResultType(Object result) {
 | 
			
		||||
        String className = toClassName(result);
 | 
			
		||||
        log.warn("Wrong result type: {}", className);
 | 
			
		||||
        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className));
 | 
			
		||||
        return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + className));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static String toClassName(Object result) {
 | 
			
		||||
        return result != null ? result.getClass().getSimpleName() : "null";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,11 +25,13 @@ import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.test.context.TestPropertySource;
 | 
			
		||||
import org.thingsboard.common.util.TbStopWatch;
 | 
			
		||||
import org.thingsboard.script.api.ScriptType;
 | 
			
		||||
import org.thingsboard.script.api.TbScriptException;
 | 
			
		||||
import org.thingsboard.script.api.js.NashornJsInvokeService;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.controller.AbstractControllerTest;
 | 
			
		||||
import org.thingsboard.server.dao.service.DaoSqlTest;
 | 
			
		||||
 | 
			
		||||
import javax.script.ScriptException;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
@ -39,6 +41,7 @@ import java.util.concurrent.TimeoutException;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 | 
			
		||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 | 
			
		||||
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
 | 
			
		||||
 | 
			
		||||
@ -59,6 +62,25 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest {
 | 
			
		||||
    @Value("${js.local.max_errors}")
 | 
			
		||||
    private int maxJsErrors;
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        var uncompilableScript = "return msg.temperature?.value;";
 | 
			
		||||
 | 
			
		||||
        // WHEN-THEN
 | 
			
		||||
        assertThatThrownBy(() -> evalScript(uncompilableScript))
 | 
			
		||||
                .isInstanceOf(ExecutionException.class)
 | 
			
		||||
                .cause()
 | 
			
		||||
                .isInstanceOf(TbScriptException.class)
 | 
			
		||||
                .asInstanceOf(type(TbScriptException.class))
 | 
			
		||||
                .satisfies(ex -> {
 | 
			
		||||
                    assertThat(ex.getScriptId()).isNotNull();
 | 
			
		||||
                    assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
 | 
			
		||||
                    assertThat(ex.getBody()).contains(uncompilableScript);
 | 
			
		||||
                    assertThat(ex.getCause()).isInstanceOf(ScriptException.class);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
 | 
			
		||||
        int iterations = 1000;
 | 
			
		||||
 | 
			
		||||
@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.mockito.ArgumentCaptor;
 | 
			
		||||
import org.springframework.test.util.ReflectionTestUtils;
 | 
			
		||||
import org.thingsboard.script.api.ScriptType;
 | 
			
		||||
import org.thingsboard.script.api.TbScriptException;
 | 
			
		||||
import org.thingsboard.server.common.data.ApiUsageState;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.stats.DefaultStatsFactory;
 | 
			
		||||
import org.thingsboard.server.common.stats.StatsCounter;
 | 
			
		||||
import org.thingsboard.server.common.stats.StatsFactory;
 | 
			
		||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
 | 
			
		||||
@ -42,8 +42,11 @@ import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.ExecutionException;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 | 
			
		||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.argThat;
 | 
			
		||||
import static org.mockito.Mockito.doAnswer;
 | 
			
		||||
@ -60,7 +63,6 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
    private RemoteJsInvokeService remoteJsInvokeService;
 | 
			
		||||
    private TbQueueRequestTemplate<TbProtoJsQueueMsg<RemoteJsRequest>, TbProtoQueueMsg<RemoteJsResponse>> jsRequestTemplate;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    public void beforeEach() {
 | 
			
		||||
        TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class);
 | 
			
		||||
@ -74,7 +76,7 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
        remoteJsInvokeService.requestTemplate = jsRequestTemplate;
 | 
			
		||||
        StatsFactory statsFactory = mock(StatsFactory.class);
 | 
			
		||||
        when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class));
 | 
			
		||||
        ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory);
 | 
			
		||||
        ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory", statsFactory);
 | 
			
		||||
        remoteJsInvokeService.init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -84,7 +86,36 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
 | 
			
		||||
    void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
 | 
			
		||||
                .setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder()
 | 
			
		||||
                        .setSuccess(false)
 | 
			
		||||
                        .setErrorCode(JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR)
 | 
			
		||||
                        .setErrorDetails("SyntaxError: Unexpected token 'const'")
 | 
			
		||||
                        .setScriptHash(methodCall.<TbProtoQueueMsg<RemoteJsRequest>>getArgument(0).getValue().getCompileRequest().getScriptHash())
 | 
			
		||||
                        .build())
 | 
			
		||||
                .build())))
 | 
			
		||||
                .when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest()));
 | 
			
		||||
 | 
			
		||||
        var uncompilableScript = "let const = 'this is not allowed';";
 | 
			
		||||
 | 
			
		||||
        // WHEN-THEN
 | 
			
		||||
        assertThatThrownBy(() -> remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, uncompilableScript).get())
 | 
			
		||||
                .isInstanceOf(ExecutionException.class)
 | 
			
		||||
                .cause()
 | 
			
		||||
                .isInstanceOf(TbScriptException.class)
 | 
			
		||||
                .asInstanceOf(type(TbScriptException.class))
 | 
			
		||||
                .satisfies(ex -> {
 | 
			
		||||
                    assertThat(ex.getScriptId()).isNotNull();
 | 
			
		||||
                    assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
 | 
			
		||||
                    assertThat(ex.getBody()).contains(uncompilableScript);
 | 
			
		||||
                    assertThat(ex.getCause()).isInstanceOf(RuntimeException.class).hasMessage("SyntaxError: Unexpected token 'const'");
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
 | 
			
		||||
        mockJsEvalResponse();
 | 
			
		||||
        String scriptBody = "return { a: 'b'};";
 | 
			
		||||
        UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
 | 
			
		||||
@ -110,7 +141,7 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
 | 
			
		||||
    void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
 | 
			
		||||
        mockJsEvalResponse();
 | 
			
		||||
        String scriptBody = "return { a: 'b'};";
 | 
			
		||||
        UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
 | 
			
		||||
@ -156,7 +187,7 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception {
 | 
			
		||||
    void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception {
 | 
			
		||||
        mockJsEvalResponse();
 | 
			
		||||
 | 
			
		||||
        TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID());
 | 
			
		||||
@ -187,7 +218,7 @@ class RemoteJsInvokeServiceTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void whenReleasingScript_thenCheckForHashUsages() throws Exception {
 | 
			
		||||
    void whenReleasingScript_thenCheckForHashUsages() throws Exception {
 | 
			
		||||
        mockJsEvalResponse();
 | 
			
		||||
        String scriptBody = "return { a: 'b'};";
 | 
			
		||||
        UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
 | 
			
		||||
 | 
			
		||||
@ -20,10 +20,12 @@ import com.github.benmanes.caffeine.cache.Cache;
 | 
			
		||||
import org.junit.Assert;
 | 
			
		||||
import org.junit.Ignore;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.mvel2.CompileException;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Value;
 | 
			
		||||
import org.springframework.test.context.TestPropertySource;
 | 
			
		||||
import org.springframework.test.util.ReflectionTestUtils;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.script.api.TbScriptException;
 | 
			
		||||
import org.thingsboard.script.api.tbel.TbelScript;
 | 
			
		||||
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
@ -37,6 +39,7 @@ import java.util.concurrent.TimeUnit;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 | 
			
		||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
 | 
			
		||||
 | 
			
		||||
@TestPropertySource(properties = {
 | 
			
		||||
        "tbel.max_script_body_size=100",
 | 
			
		||||
@ -50,6 +53,25 @@ class TbelInvokeServiceTest extends AbstractTbelInvokeTest {
 | 
			
		||||
    @Value("${tbel.max_errors}")
 | 
			
		||||
    private int maxJsErrors;
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        var uncompilableScript = "return msg.property !== undefined;";
 | 
			
		||||
 | 
			
		||||
        // WHEN-THEN
 | 
			
		||||
        assertThatThrownBy(() -> evalScript(uncompilableScript))
 | 
			
		||||
                .isInstanceOf(ExecutionException.class)
 | 
			
		||||
                .cause()
 | 
			
		||||
                .isInstanceOf(TbScriptException.class)
 | 
			
		||||
                .asInstanceOf(type(TbScriptException.class))
 | 
			
		||||
                .satisfies(ex -> {
 | 
			
		||||
                    assertThat(ex.getScriptId()).isNotNull();
 | 
			
		||||
                    assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
 | 
			
		||||
                    assertThat(ex.getBody()).isEqualTo(uncompilableScript);
 | 
			
		||||
                    assertThat(ex.getCause()).isInstanceOf(CompileException.class);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
 | 
			
		||||
        int iterations = 100000;
 | 
			
		||||
 | 
			
		||||
@ -19,8 +19,8 @@ import lombok.Getter;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.thingsboard.server.common.data.EntityType;
 | 
			
		||||
import org.thingsboard.common.util.RecoveryAware;
 | 
			
		||||
import org.thingsboard.server.common.msg.MsgType;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbActorError;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbActorMsg;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbActorStopReason;
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import java.util.function.Supplier;
 | 
			
		||||
@Getter
 | 
			
		||||
@RequiredArgsConstructor
 | 
			
		||||
public final class TbActorMailbox implements TbActorCtx {
 | 
			
		||||
 | 
			
		||||
    private static final boolean HIGH_PRIORITY = true;
 | 
			
		||||
    private static final boolean NORMAL_PRIORITY = false;
 | 
			
		||||
 | 
			
		||||
@ -100,7 +101,7 @@ public final class TbActorMailbox implements TbActorCtx {
 | 
			
		||||
        if (t instanceof TbActorException && t.getCause() != null) {
 | 
			
		||||
            t = t.getCause();
 | 
			
		||||
        }
 | 
			
		||||
        return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable();
 | 
			
		||||
        return t instanceof RecoveryAware recoveryAware && recoveryAware.isUnrecoverable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void enqueue(TbActorMsg msg, boolean highPriority) {
 | 
			
		||||
 | 
			
		||||
@ -16,13 +16,24 @@
 | 
			
		||||
package org.thingsboard.script.api;
 | 
			
		||||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import org.thingsboard.common.util.RecoveryAware;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
 | 
			
		||||
public class TbScriptException extends RuntimeException {
 | 
			
		||||
public class TbScriptException extends RuntimeException implements RecoveryAware {
 | 
			
		||||
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = -1958193538782818284L;
 | 
			
		||||
 | 
			
		||||
    public static enum ErrorCode {COMPILATION, TIMEOUT, RUNTIME, OTHER}
 | 
			
		||||
    public enum ErrorCode {
 | 
			
		||||
 | 
			
		||||
        COMPILATION,
 | 
			
		||||
        TIMEOUT,
 | 
			
		||||
        RUNTIME,
 | 
			
		||||
        OTHER
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final UUID scriptId;
 | 
			
		||||
@ -37,4 +48,10 @@ public class TbScriptException extends RuntimeException {
 | 
			
		||||
        this.errorCode = errorCode;
 | 
			
		||||
        this.body = body;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isUnrecoverable() {
 | 
			
		||||
        return errorCode == ErrorCode.COMPILATION;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListeningExecutorService;
 | 
			
		||||
import com.google.common.util.concurrent.MoreExecutors;
 | 
			
		||||
import delight.nashornsandbox.NashornSandbox;
 | 
			
		||||
import delight.nashornsandbox.NashornSandboxes;
 | 
			
		||||
import delight.nashornsandbox.exceptions.ScriptCPUAbuseException;
 | 
			
		||||
import jakarta.annotation.PostConstruct;
 | 
			
		||||
import jakarta.annotation.PreDestroy;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
@ -153,8 +154,12 @@ public class NashornJsInvokeService extends AbstractJsInvokeService {
 | 
			
		||||
                }
 | 
			
		||||
                scriptInfoMap.put(scriptId, scriptInfo);
 | 
			
		||||
                return scriptId;
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
            } catch (ScriptException e) {
 | 
			
		||||
                throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e);
 | 
			
		||||
            } catch (ScriptCPUAbuseException e) {
 | 
			
		||||
                throw new TbScriptException(scriptId, TbScriptException.ErrorCode.TIMEOUT, jsScript, e);
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, jsScript, e);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import jakarta.annotation.PreDestroy;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.mvel2.CompileException;
 | 
			
		||||
import org.mvel2.ExecutionContext;
 | 
			
		||||
import org.mvel2.MVEL;
 | 
			
		||||
import org.mvel2.ParserContext;
 | 
			
		||||
@ -52,11 +53,11 @@ import java.io.Serializable;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Calendar;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
import java.util.UUID;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.concurrent.ConcurrentMap;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.locks.Lock;
 | 
			
		||||
import java.util.concurrent.locks.ReentrantLock;
 | 
			
		||||
@ -66,9 +67,9 @@ import java.util.concurrent.locks.ReentrantLock;
 | 
			
		||||
@Service
 | 
			
		||||
public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService {
 | 
			
		||||
 | 
			
		||||
    protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
 | 
			
		||||
    protected final Map<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
 | 
			
		||||
    protected Cache<String, Serializable> compiledScriptsCache;
 | 
			
		||||
    private final ConcurrentMap<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
 | 
			
		||||
    private final ConcurrentMap<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
 | 
			
		||||
    private Cache<String, Serializable> compiledScriptsCache;
 | 
			
		||||
 | 
			
		||||
    private SandboxedParserConfiguration parserConfig;
 | 
			
		||||
    private final Optional<TbApiUsageStateClient> apiUsageStateClient;
 | 
			
		||||
@ -204,8 +205,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
 | 
			
		||||
                    lock.unlock();
 | 
			
		||||
                }
 | 
			
		||||
                return scriptId;
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
            } catch (CompileException e) {
 | 
			
		||||
                throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, scriptBody, e);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
@ -246,7 +249,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Serializable compileScript(String scriptBody) {
 | 
			
		||||
    private static Serializable compileScript(String scriptBody) throws CompileException {
 | 
			
		||||
        return MVEL.compileExpression(scriptBody, new ParserContext());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -269,4 +272,5 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
 | 
			
		||||
    protected StatsType getStatsType() {
 | 
			
		||||
        return StatsType.TBEL_INVOKE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2025 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;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.EnumSource;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
 | 
			
		||||
class TbScriptExceptionTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void givenCompilationError_whenCheckingIsUnrecoverable_thenReturnsTrue() {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        var exception = new TbScriptException(null, TbScriptException.ErrorCode.COMPILATION, null, null);
 | 
			
		||||
 | 
			
		||||
        // WHEN-THEN
 | 
			
		||||
        assertThat(exception.isUnrecoverable()).isTrue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ParameterizedTest
 | 
			
		||||
    @EnumSource(
 | 
			
		||||
            value = TbScriptException.ErrorCode.class,
 | 
			
		||||
            mode = EnumSource.Mode.EXCLUDE,
 | 
			
		||||
            names = "COMPILATION"
 | 
			
		||||
    )
 | 
			
		||||
    void givenRecoverableErrorCodes_whenCheckingIsUnrecoverable_thenReturnsFalse(TbScriptException.ErrorCode errorCode) {
 | 
			
		||||
        // GIVEN
 | 
			
		||||
        var exception = new TbScriptException(null, errorCode, null, null);
 | 
			
		||||
 | 
			
		||||
        // WHEN-THEN
 | 
			
		||||
        assertThat(exception.isUnrecoverable()).isFalse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -13,9 +13,9 @@
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.common.msg;
 | 
			
		||||
package org.thingsboard.common.util;
 | 
			
		||||
 | 
			
		||||
public interface TbActorError {
 | 
			
		||||
public interface RecoveryAware {
 | 
			
		||||
 | 
			
		||||
    boolean isUnrecoverable();
 | 
			
		||||
 | 
			
		||||
@ -207,14 +207,14 @@ final class MqttClientImpl implements MqttClient {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void scheduleConnectIfRequired(String host, int port, boolean reconnect) {
 | 
			
		||||
        log.trace("[{}] Scheduling connect to server, isReconnect - {}", channel != null ? channel.id() : "UNKNOWN", reconnect);
 | 
			
		||||
        log.trace("[{}][{}][{}] Scheduling connect to server, isReconnect - {}", host, port, channel != null ? channel.id() : "UNKNOWN", reconnect);
 | 
			
		||||
        if (clientConfig.isReconnect() && !disconnected) {
 | 
			
		||||
            if (reconnect) {
 | 
			
		||||
                this.reconnect = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            final long nextReconnectDelay = reconnectStrategy.getNextReconnectDelay();
 | 
			
		||||
            log.info("[{}] Scheduling reconnect in [{}] sec", channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay);
 | 
			
		||||
            log.debug("[{}][{}][{}] Scheduling reconnect in [{}] sec", host, port, channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay);
 | 
			
		||||
            eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), nextReconnectDelay, TimeUnit.SECONDS);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -16,12 +16,9 @@
 | 
			
		||||
package org.thingsboard.rule.engine.api;
 | 
			
		||||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
import org.thingsboard.server.common.msg.TbActorError;
 | 
			
		||||
import org.thingsboard.common.util.RecoveryAware;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Created by ashvayka on 19.01.18.
 | 
			
		||||
 */
 | 
			
		||||
public class TbNodeException extends Exception implements TbActorError {
 | 
			
		||||
public class TbNodeException extends Exception implements RecoveryAware {
 | 
			
		||||
 | 
			
		||||
    @Getter
 | 
			
		||||
    private final boolean unrecoverable;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user