first-fix
This commit is contained in:
1 parent
6d87c1edc8
commit
565d9d673a
6284 files changed
+820267
-76
No files matched your search
@@ -0,0 +1,124 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2023 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.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<version>0.6.2-SNAPSHOT</version>
|
||||
<artifactId>script</artifactId>
|
||||
</parent>
|
||||
<groupId>org.thingsboard.common.script</groupId>
|
||||
<artifactId>script-api</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Thingsboard Server Script invoke API</name>
|
||||
<url>https://thingsboard.io</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<main.dir>${basedir}/../../..</main.dir>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<artifactId>data</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<artifactId>stats</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard.common</groupId>
|
||||
<artifactId>util</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.javadelight</groupId>
|
||||
<artifactId>delight-nashorn-sandbox</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>log4j-over-slf4j</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thingsboard</groupId>
|
||||
<artifactId>tbel</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>thingsboard-repo-deploy</id>
|
||||
<name>ThingsBoard Repo Deployment</name>
|
||||
<url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
</project>
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.FutureCallback;
|
||||
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.common.util.ThingsBoardThreadFactory;
|
||||
import org.thingsboard.server.common.data.ApiUsageRecordKey;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractScriptInvokeService implements ScriptInvokeService {
|
||||
|
||||
protected final Map<UUID, BlockedScriptInfo> disabledScripts = new ConcurrentHashMap<>();
|
||||
|
||||
private final Optional<TbApiUsageStateClient> apiUsageStateClient;
|
||||
private final Optional<TbApiUsageReportClient> apiUsageReportClient;
|
||||
private final AtomicInteger pushedMsgs = new AtomicInteger(0);
|
||||
private final AtomicInteger invokeMsgs = new AtomicInteger(0);
|
||||
private final AtomicInteger evalMsgs = new AtomicInteger(0);
|
||||
protected final AtomicInteger failedMsgs = new AtomicInteger(0);
|
||||
protected final AtomicInteger timeoutMsgs = new AtomicInteger(0);
|
||||
|
||||
private final FutureCallback<UUID> evalCallback = new ScriptStatCallback<>(evalMsgs, timeoutMsgs, failedMsgs);
|
||||
private final FutureCallback<Object> invokeCallback = new ScriptStatCallback<>(invokeMsgs, timeoutMsgs, failedMsgs);
|
||||
|
||||
protected ScheduledExecutorService timeoutExecutorService;
|
||||
|
||||
protected AbstractScriptInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
|
||||
this.apiUsageStateClient = apiUsageStateClient;
|
||||
this.apiUsageReportClient = apiUsageReportClient;
|
||||
}
|
||||
|
||||
protected long getMaxEvalRequestsTimeout() {
|
||||
return getMaxInvokeRequestsTimeout();
|
||||
}
|
||||
|
||||
protected abstract long getMaxInvokeRequestsTimeout();
|
||||
|
||||
protected abstract long getMaxScriptBodySize();
|
||||
|
||||
protected abstract long getMaxTotalArgsSize();
|
||||
|
||||
protected abstract long getMaxResultSize();
|
||||
|
||||
protected abstract int getMaxBlackListDurationSec();
|
||||
|
||||
protected abstract int getMaxErrors();
|
||||
|
||||
protected abstract boolean isStatsEnabled();
|
||||
|
||||
protected abstract String getStatsName();
|
||||
|
||||
protected abstract Executor getCallbackExecutor();
|
||||
|
||||
protected abstract boolean isScriptPresent(UUID scriptId);
|
||||
|
||||
protected abstract ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames);
|
||||
|
||||
protected abstract TbScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args);
|
||||
|
||||
protected abstract void doRelease(UUID scriptId) throws Exception;
|
||||
|
||||
public void init() {
|
||||
if (getMaxEvalRequestsTimeout() > 0 || getMaxInvokeRequestsTimeout() > 0) {
|
||||
timeoutExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("script-timeout"));
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (timeoutExecutorService != null) {
|
||||
timeoutExecutorService.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void printStats() {
|
||||
if (isStatsEnabled()) {
|
||||
int pushed = pushedMsgs.getAndSet(0);
|
||||
int invoked = invokeMsgs.getAndSet(0);
|
||||
int evaluated = evalMsgs.getAndSet(0);
|
||||
int failed = failedMsgs.getAndSet(0);
|
||||
int timedOut = timeoutMsgs.getAndSet(0);
|
||||
if (pushed > 0 || invoked > 0 || evaluated > 0 || failed > 0 || timedOut > 0) {
|
||||
log.info("{}: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
|
||||
getStatsName(), pushed, invoked + evaluated, invoked, evaluated, failed, timedOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) {
|
||||
if (!apiUsageStateClient.isPresent() || apiUsageStateClient.get().getApiUsageState(tenantId).isJsExecEnabled()) {
|
||||
if (scriptBodySizeExceeded(scriptBody)) {
|
||||
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
|
||||
}
|
||||
UUID scriptId = UUID.randomUUID();
|
||||
pushedMsgs.incrementAndGet();
|
||||
return withTimeoutAndStatsCallback(scriptId, null,
|
||||
doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout());
|
||||
} else {
|
||||
return error("Script Execution is disabled due to API limits!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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 + "]!");
|
||||
}
|
||||
if (!isDisabled(scriptId)) {
|
||||
if (argsSizeExceeded(args)) {
|
||||
TbScriptException t = new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new IllegalArgumentException(
|
||||
format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize())
|
||||
));
|
||||
return Futures.immediateFailedFuture(handleScriptException(scriptId, null, t));
|
||||
}
|
||||
apiUsageReportClient.ifPresent(client -> client.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1));
|
||||
pushedMsgs.incrementAndGet();
|
||||
log.trace("InvokeScript uuid {} with timeout {}ms", scriptId, getMaxInvokeRequestsTimeout());
|
||||
var task = doInvokeFunction(scriptId, args);
|
||||
|
||||
var resultFuture = Futures.transformAsync(task.getResultFuture(), output -> {
|
||||
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(output);
|
||||
}, MoreExecutors.directExecutor());
|
||||
|
||||
return withTimeoutAndStatsCallback(scriptId, task, resultFuture, invokeCallback, getMaxInvokeRequestsTimeout());
|
||||
} else {
|
||||
String message = "Script invocation is blocked due to maximum error count "
|
||||
+ getMaxErrors() + ", scriptId " + scriptId + "!";
|
||||
log.warn(message);
|
||||
return error(message);
|
||||
}
|
||||
} else {
|
||||
return error("Script execution is disabled due to API limits!");
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends V, V> ListenableFuture<T> withTimeoutAndStatsCallback(UUID scriptId, TbScriptExecutionTask task, ListenableFuture<T> future, FutureCallback<V> statsCallback, long timeout) {
|
||||
if (timeout > 0) {
|
||||
future = Futures.withTimeout(future, timeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
|
||||
}
|
||||
Futures.addCallback(future, statsCallback, getCallbackExecutor());
|
||||
return Futures.catchingAsync(future, Exception.class,
|
||||
input -> Futures.immediateFailedFuture(handleScriptException(scriptId, task, input)),
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private Throwable handleScriptException(UUID scriptId, TbScriptExecutionTask task, Throwable t) {
|
||||
boolean timeout = t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException);
|
||||
if (timeout && task != null) {
|
||||
task.stop();
|
||||
}
|
||||
boolean blockList = timeout;
|
||||
String scriptBody = null;
|
||||
if (t instanceof TbScriptException) {
|
||||
var scriptException = (TbScriptException) t;
|
||||
scriptBody = scriptException.getBody();
|
||||
var cause = scriptException.getCause();
|
||||
switch (scriptException.getErrorCode()) {
|
||||
case COMPILATION:
|
||||
log.debug("[{}] Failed to compile script: {}", scriptId, scriptException.getBody(), cause);
|
||||
break;
|
||||
case TIMEOUT:
|
||||
log.debug("[{}] Timeout to execute script: {}", scriptId, scriptException.getBody(), cause);
|
||||
break;
|
||||
case OTHER:
|
||||
case RUNTIME:
|
||||
log.debug("[{}] Failed to execute script: {}", scriptId, scriptException.getBody(), cause);
|
||||
break;
|
||||
}
|
||||
blockList = timeout || scriptException.getErrorCode() != TbScriptException.ErrorCode.RUNTIME;
|
||||
}
|
||||
if (blockList) {
|
||||
BlockedScriptInfo disableListInfo = disabledScripts.computeIfAbsent(scriptId, key -> new BlockedScriptInfo(getMaxBlackListDurationSec()));
|
||||
int counter = disableListInfo.incrementAndGet();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Script has exception counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
|
||||
counter, scriptId, t, t.getCause(), scriptBody);
|
||||
} else {
|
||||
log.warn("Script has exception counter {} on disabledFunctions for id {}, exception {}",
|
||||
counter, scriptId, t.getMessage());
|
||||
}
|
||||
}
|
||||
if (timeout) {
|
||||
return new TimeoutException("Script timeout!");
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Void> release(UUID scriptId) {
|
||||
if (isScriptPresent(scriptId)) {
|
||||
try {
|
||||
disabledScripts.remove(scriptId);
|
||||
doRelease(scriptId);
|
||||
} catch (Exception e) {
|
||||
return Futures.immediateFailedFuture(e);
|
||||
}
|
||||
}
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
|
||||
private boolean isDisabled(UUID scriptId) {
|
||||
BlockedScriptInfo errorCount = disabledScripts.get(scriptId);
|
||||
if (errorCount != null) {
|
||||
if (errorCount.getExpirationTime() <= System.currentTimeMillis()) {
|
||||
disabledScripts.remove(scriptId);
|
||||
return false;
|
||||
} else {
|
||||
return errorCount.get() >= getMaxErrors();
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean scriptBodySizeExceeded(String scriptBody) {
|
||||
if (getMaxScriptBodySize() <= 0) return false;
|
||||
return scriptBody.length() > getMaxScriptBodySize();
|
||||
}
|
||||
|
||||
private boolean argsSizeExceeded(Object[] args) {
|
||||
if (getMaxTotalArgsSize() <= 0) return false;
|
||||
long totalArgsSize = 0;
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof CharSequence) {
|
||||
totalArgsSize += ((CharSequence) arg).length();
|
||||
} else {
|
||||
var str = JacksonUtil.toString(arg);
|
||||
if (str != null) {
|
||||
totalArgsSize += str.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalArgsSize > getMaxTotalArgsSize();
|
||||
}
|
||||
|
||||
private boolean resultSizeExceeded(String result) {
|
||||
if (getMaxResultSize() <= 0) return false;
|
||||
return result != null && result.length() > getMaxResultSize();
|
||||
}
|
||||
|
||||
private <T> ListenableFuture<T> error(String message) {
|
||||
return Futures.immediateFailedFuture(new RuntimeException(message));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class BlockedScriptInfo {
|
||||
private final long maxScriptBlockDurationMs;
|
||||
private final AtomicInteger counter;
|
||||
private long expirationTime;
|
||||
|
||||
BlockedScriptInfo(int maxScriptBlockDuration) {
|
||||
this.maxScriptBlockDurationMs = TimeUnit.SECONDS.toMillis(maxScriptBlockDuration);
|
||||
this.counter = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
public int get() {
|
||||
return counter.get();
|
||||
}
|
||||
|
||||
public int incrementAndGet() {
|
||||
int result = counter.incrementAndGet();
|
||||
expirationTime = System.currentTimeMillis() + maxScriptBlockDurationMs;
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getExpirationTime() {
|
||||
return expirationTime;
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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;
|
||||
|
||||
public class RuleNodeScriptFactory {
|
||||
|
||||
public static final String MSG = "msg";
|
||||
public static final String METADATA = "metadata";
|
||||
public static final String MSG_TYPE = "msgType";
|
||||
public static final String RULE_NODE_FUNCTION_NAME = "ruleNodeFunc";
|
||||
|
||||
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
|
||||
" var msg = JSON.parse(msgStr); " +
|
||||
" var metadata = JSON.parse(metadataStr); " +
|
||||
" return JSON.stringify(%s(msg, metadata, msgType));" +
|
||||
" function %s(%s, %s, %s) {";
|
||||
private static final String JS_WRAPPER_SUFFIX = "\n}" +
|
||||
"\n}";
|
||||
|
||||
|
||||
public static String generateRuleNodeScript(String functionName, String scriptBody, String... argNames) {
|
||||
String msgArg;
|
||||
String metadataArg;
|
||||
String msgTypeArg;
|
||||
if (argNames != null && argNames.length == 3) {
|
||||
msgArg = argNames[0];
|
||||
metadataArg = argNames[1];
|
||||
msgTypeArg = argNames[2];
|
||||
} else {
|
||||
msgArg = MSG;
|
||||
metadataArg = METADATA;
|
||||
msgTypeArg = MSG_TYPE;
|
||||
}
|
||||
String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName,
|
||||
RULE_NODE_FUNCTION_NAME, RULE_NODE_FUNCTION_NAME, msgArg, metadataArg, msgTypeArg);
|
||||
return jsWrapperPrefix + scriptBody + JS_WRAPPER_SUFFIX;
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.script.ScriptLanguage;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ScriptInvokeService {
|
||||
|
||||
ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames);
|
||||
|
||||
ListenableFuture<Object> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
|
||||
|
||||
ListenableFuture<Void> release(UUID scriptId);
|
||||
|
||||
ScriptLanguage getLanguage();
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.FutureCallback;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class ScriptStatCallback<T> implements FutureCallback<T> {
|
||||
|
||||
private final AtomicInteger successMsgs;
|
||||
private final AtomicInteger timeoutMsgs;
|
||||
private final AtomicInteger failedMsgs;
|
||||
|
||||
@Override
|
||||
public void onSuccess(@Nullable T result) {
|
||||
successMsgs.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
|
||||
timeoutMsgs.incrementAndGet();
|
||||
} else {
|
||||
failedMsgs.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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;
|
||||
|
||||
public enum ScriptType {
|
||||
RULE_NODE_SCRIPT
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class TbScriptException extends RuntimeException {
|
||||
private static final long serialVersionUID = -1958193538782818284L;
|
||||
|
||||
public static enum ErrorCode {COMPILATION, TIMEOUT, RUNTIME, OTHER}
|
||||
|
||||
@Getter
|
||||
private final UUID scriptId;
|
||||
@Getter
|
||||
private final ErrorCode errorCode;
|
||||
@Getter
|
||||
private final String body;
|
||||
|
||||
public TbScriptException(UUID scriptId, ErrorCode errorCode, String body, Exception cause) {
|
||||
super(cause);
|
||||
this.scriptId = scriptId;
|
||||
this.errorCode = errorCode;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class TbScriptExecutionTask {
|
||||
|
||||
@Getter
|
||||
private final ListenableFuture<Object> resultFuture;
|
||||
|
||||
public abstract void stop();
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.hash.Hashing;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.thingsboard.script.api.AbstractScriptInvokeService;
|
||||
import org.thingsboard.script.api.RuleNodeScriptFactory;
|
||||
import org.thingsboard.script.api.ScriptType;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Created by ashvayka on 26.09.18.
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService implements JsInvokeService {
|
||||
|
||||
protected final Map<UUID, JsScriptInfo> scriptInfoMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Getter
|
||||
@Value("${js.max_total_args_size:100000}")
|
||||
private long maxTotalArgsSize;
|
||||
@Getter
|
||||
@Value("${js.max_result_size:300000}")
|
||||
private long maxResultSize;
|
||||
@Getter
|
||||
@Value("${js.max_script_body_size:50000}")
|
||||
private long maxScriptBodySize;
|
||||
|
||||
protected AbstractJsInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
|
||||
super(apiUsageStateClient, apiUsageReportClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isScriptPresent(UUID scriptId) {
|
||||
return scriptInfoMap.containsKey(scriptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
|
||||
return new JsScriptExecutionTask(doInvokeFunction(scriptId, scriptInfoMap.get(scriptId), args));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) {
|
||||
String scriptHash = hash(tenantId, scriptBody);
|
||||
String functionName = constructFunctionName(scriptId, scriptHash);
|
||||
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
|
||||
return doEval(scriptId, new JsScriptInfo(scriptHash, functionName), jsScript);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doRelease(UUID scriptId) throws Exception {
|
||||
doRelease(scriptId, scriptInfoMap.remove(scriptId));
|
||||
}
|
||||
|
||||
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody);
|
||||
|
||||
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, Object[] args);
|
||||
|
||||
protected abstract void doRelease(UUID scriptId, JsScriptInfo scriptInfo) throws Exception;
|
||||
|
||||
private String generateJsScript(ScriptType scriptType, String functionName, String scriptBody, String... argNames) {
|
||||
if (scriptType == ScriptType.RULE_NODE_SCRIPT) {
|
||||
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
|
||||
}
|
||||
throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
|
||||
}
|
||||
|
||||
protected String constructFunctionName(UUID scriptId, String scriptHash) {
|
||||
return "invokeInternal_" + scriptId.toString().replace('-', '_');
|
||||
}
|
||||
|
||||
protected String hash(TenantId tenantId, String scriptBody) {
|
||||
return Hashing.murmur3_128().newHasher()
|
||||
.putLong(tenantId.getId().getMostSignificantBits())
|
||||
.putLong(tenantId.getId().getLeastSignificantBits())
|
||||
.putUnencodedChars(scriptBody)
|
||||
.hash().toString();
|
||||
}
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 The Thingsboard Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.thingsboard.script.api.js;
|
||||
|
||||
import org.thingsboard.script.api.ScriptInvokeService;
|
||||
import org.thingsboard.server.common.data.script.ScriptLanguage;
|
||||
|
||||
public interface JsInvokeService extends ScriptInvokeService {
|
||||
|
||||
@Override
|
||||
default ScriptLanguage getLanguage() {
|
||||
return ScriptLanguage.JS;
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.thingsboard.script.api.TbScriptExecutionTask;
|
||||
|
||||
public class JsScriptExecutionTask extends TbScriptExecutionTask {
|
||||
|
||||
public JsScriptExecutionTask(ListenableFuture<Object> resultFuture) {
|
||||
super(resultFuture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 lombok.Data;
|
||||
|
||||
@Data
|
||||
public class JsScriptInfo {
|
||||
|
||||
private final String hash;
|
||||
private final String functionName;
|
||||
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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 com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import delight.nashornsandbox.NashornSandbox;
|
||||
import delight.nashornsandbox.NashornSandboxes;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.thingsboard.common.util.ThingsBoardExecutors;
|
||||
import org.thingsboard.script.api.TbScriptException;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "local", matchIfMissing = true)
|
||||
@Service
|
||||
public class NashornJsInvokeService extends AbstractJsInvokeService {
|
||||
|
||||
private NashornSandbox sandbox;
|
||||
private ScriptEngine engine;
|
||||
private ExecutorService monitorExecutorService;
|
||||
private ListeningExecutorService jsExecutor;
|
||||
|
||||
private final ReentrantLock evalLock = new ReentrantLock();
|
||||
|
||||
@Value("${js.local.use_js_sandbox}")
|
||||
private boolean useJsSandbox;
|
||||
|
||||
@Value("${js.local.monitor_thread_pool_size}")
|
||||
private int monitorThreadPoolSize;
|
||||
|
||||
@Value("${js.local.max_cpu_time}")
|
||||
private long maxCpuTime;
|
||||
|
||||
@Getter
|
||||
@Value("${js.local.max_errors}")
|
||||
private int maxErrors;
|
||||
|
||||
@Getter
|
||||
@Value("${js.local.max_black_list_duration_sec:60}")
|
||||
private int maxBlackListDurationSec;
|
||||
|
||||
@Getter
|
||||
@Value("${js.local.max_requests_timeout:0}")
|
||||
private long maxInvokeRequestsTimeout;
|
||||
|
||||
@Getter
|
||||
@Value("${js.local.stats.enabled:false}")
|
||||
private boolean statsEnabled;
|
||||
|
||||
@Value("${js.local.js_thread_pool_size:50}")
|
||||
private int jsExecutorThreadPoolSize;
|
||||
|
||||
public NashornJsInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
|
||||
super(apiUsageStateClient, apiUsageReportClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStatsName() {
|
||||
return "Nashorn JS Invoke Stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Executor getCallbackExecutor() {
|
||||
return MoreExecutors.directExecutor();
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${js.local.stats.print_interval_ms:10000}")
|
||||
public void printStats() {
|
||||
super.printStats();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init() {
|
||||
super.init();
|
||||
jsExecutor = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(jsExecutorThreadPoolSize));
|
||||
if (useJsSandbox) {
|
||||
sandbox = NashornSandboxes.create();
|
||||
monitorExecutorService = ThingsBoardExecutors.newWorkStealingPool(monitorThreadPoolSize, "nashorn-js-monitor");
|
||||
sandbox.setExecutor(monitorExecutorService);
|
||||
sandbox.setMaxCPUTime(maxCpuTime);
|
||||
sandbox.allowNoBraces(false);
|
||||
sandbox.allowLoadFunctions(true);
|
||||
sandbox.setMaxPreparedStatements(30);
|
||||
} else {
|
||||
ScriptEngineManager factory = new ScriptEngineManager();
|
||||
engine = factory.getEngineByName("nashorn");
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (monitorExecutorService != null) {
|
||||
monitorExecutorService.shutdownNow();
|
||||
}
|
||||
if (jsExecutor != null) {
|
||||
jsExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<UUID> doEval(UUID scriptId, JsScriptInfo scriptInfo, String jsScript) {
|
||||
return jsExecutor.submit(() -> {
|
||||
try {
|
||||
evalLock.lock();
|
||||
try {
|
||||
if (useJsSandbox) {
|
||||
sandbox.eval(jsScript);
|
||||
} else {
|
||||
engine.eval(jsScript);
|
||||
}
|
||||
} finally {
|
||||
evalLock.unlock();
|
||||
}
|
||||
scriptInfoMap.put(scriptId, scriptInfo);
|
||||
return scriptId;
|
||||
} catch (Exception e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, JsScriptInfo scriptInfo, Object[] args) {
|
||||
return jsExecutor.submit(() -> {
|
||||
try {
|
||||
if (useJsSandbox) {
|
||||
return sandbox.getSandboxedInvocable().invokeFunction(scriptInfo.getFunctionName(), args);
|
||||
} else {
|
||||
return ((Invocable) engine).invokeFunction(scriptInfo.getFunctionName(), args);
|
||||
}
|
||||
} catch (ScriptException e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, e);
|
||||
} catch (Exception e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void doRelease(UUID scriptId, JsScriptInfo scriptInfo) throws ScriptException {
|
||||
if (useJsSandbox) {
|
||||
sandbox.eval(scriptInfo.getFunctionName() + " = undefined;");
|
||||
} else {
|
||||
engine.eval(scriptInfo.getFunctionName() + " = undefined;");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.google.common.hash.Hasher;
|
||||
import com.google.common.hash.Hashing;
|
||||
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.CompileException;
|
||||
import org.mvel2.ExecutionContext;
|
||||
import org.mvel2.MVEL;
|
||||
import org.mvel2.ParserContext;
|
||||
import org.mvel2.SandboxedParserConfiguration;
|
||||
import org.mvel2.ScriptMemoryOverflowException;
|
||||
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.thingsboard.common.util.ThingsBoardExecutors;
|
||||
import org.thingsboard.script.api.AbstractScriptInvokeService;
|
||||
import org.thingsboard.script.api.ScriptType;
|
||||
import org.thingsboard.script.api.TbScriptException;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
|
||||
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
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.Executor;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(prefix = "tbel", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@Service
|
||||
public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService {
|
||||
|
||||
protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
|
||||
protected final Map<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
|
||||
private final String tbelSwitch = "switch";
|
||||
private final String tbelSwitchErrorMsg = "TBEL does not support the 'switch'.";
|
||||
protected Cache<String, Serializable> compiledScriptsCache;
|
||||
|
||||
private SandboxedParserConfiguration parserConfig;
|
||||
|
||||
@Getter
|
||||
@Value("${tbel.max_total_args_size:100000}")
|
||||
private long maxTotalArgsSize;
|
||||
@Getter
|
||||
@Value("${tbel.max_result_size:300000}")
|
||||
private long maxResultSize;
|
||||
@Getter
|
||||
@Value("${tbel.max_script_body_size:50000}")
|
||||
private long maxScriptBodySize;
|
||||
|
||||
@Getter
|
||||
@Value("${tbel.max_errors:3}")
|
||||
private int maxErrors;
|
||||
|
||||
@Getter
|
||||
@Value("${tbel.max_black_list_duration_sec:60}")
|
||||
private int maxBlackListDurationSec;
|
||||
|
||||
@Getter
|
||||
@Value("${tbel.max_requests_timeout:0}")
|
||||
private long maxInvokeRequestsTimeout;
|
||||
|
||||
@Getter
|
||||
@Value("${tbel.stats.enabled:false}")
|
||||
private boolean statsEnabled;
|
||||
|
||||
@Value("${tbel.thread_pool_size:50}")
|
||||
private int threadPoolSize;
|
||||
|
||||
@Value("${tbel.max_memory_limit_mb:8}")
|
||||
private long maxMemoryLimitMb;
|
||||
|
||||
@Value("${tbel.compiled_scripts_cache_size:1000}")
|
||||
private int compiledScriptsCacheSize;
|
||||
|
||||
private ListeningExecutorService executor;
|
||||
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
protected DefaultTbelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
|
||||
super(apiUsageStateClient, apiUsageReportClient);
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${tbel.stats.print_interval_ms:10000}")
|
||||
public void printStats() {
|
||||
super.printStats();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init() {
|
||||
super.init();
|
||||
OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE);
|
||||
parserConfig = ParserContext.enableSandboxedMode();
|
||||
parserConfig.addImport("JSON", TbJson.class);
|
||||
parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
|
||||
parserConfig.registerDataType("Random", Random.class, date -> 8L);
|
||||
parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L);
|
||||
TbUtils.register(parserConfig);
|
||||
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
|
||||
try {
|
||||
// Special command to warm up TBEL engine
|
||||
Serializable script = compileScript("var warmUp = {}; warmUp");
|
||||
MVEL.executeTbExpression(script, new ExecutionContext(parserConfig), Collections.emptyMap());
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
compiledScriptsCache = Caffeine.newBuilder()
|
||||
.maximumSize(compiledScriptsCacheSize)
|
||||
.build();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStatsName() {
|
||||
return "TBEL Scripts Stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Executor getCallbackExecutor() {
|
||||
return MoreExecutors.directExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isScriptPresent(UUID scriptId) {
|
||||
return scriptIdToHash.containsKey(scriptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) {
|
||||
return executor.submit(() -> {
|
||||
try {
|
||||
String scriptHash = hash(scriptBody, argNames);
|
||||
compiledScriptsCache.get(scriptHash, k -> compileScript(scriptBody));
|
||||
lock.lock();
|
||||
try {
|
||||
scriptIdToHash.put(scriptId, scriptHash);
|
||||
scriptMap.computeIfAbsent(scriptHash, k -> new TbelScript(scriptBody, argNames));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
return scriptId;
|
||||
} catch (CompileException ce) {
|
||||
if ( ce.getExpr() != null && new String(ce.getExpr()).contains(tbelSwitch)) {
|
||||
ce = new CompileException(tbelSwitchErrorMsg, ce.getExpr(), ce.getCursor(), ce.getCause());
|
||||
}
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, ce);
|
||||
} catch (Exception e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TbelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
|
||||
ExecutionContext executionContext = new ExecutionContext(this.parserConfig, maxMemoryLimitMb * 1024 * 1024);
|
||||
return new TbelScriptExecutionTask(executionContext, executor.submit(() -> {
|
||||
String scriptHash = scriptIdToHash.get(scriptId);
|
||||
if (scriptHash == null) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!"));
|
||||
}
|
||||
TbelScript script = scriptMap.get(scriptHash);
|
||||
Serializable compiledScript = compiledScriptsCache.get(scriptHash, k -> compileScript(script.getScriptBody()));
|
||||
try {
|
||||
return MVEL.executeTbExpression(compiledScript, executionContext, script.createVars(args));
|
||||
} catch (ScriptMemoryOverflowException e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Script memory overflow!"));
|
||||
} catch (Exception e) {
|
||||
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, script.getScriptBody(), e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doRelease(UUID scriptId) {
|
||||
String scriptHash = scriptIdToHash.remove(scriptId);
|
||||
if (scriptHash != null) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!scriptIdToHash.containsValue(scriptHash)) {
|
||||
scriptMap.remove(scriptHash);
|
||||
compiledScriptsCache.invalidate(scriptHash);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Serializable compileScript(String scriptBody) {
|
||||
return MVEL.compileExpression(scriptBody, new ParserContext());
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
protected String hash(String scriptBody, String[] argNames) {
|
||||
Hasher hasher = Hashing.murmur3_128().newHasher();
|
||||
hasher.putUnencodedChars(scriptBody);
|
||||
for (String argName : argNames) {
|
||||
hasher.putString(argName, StandardCharsets.UTF_8);
|
||||
}
|
||||
return hasher.hash().toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class TbDate extends Date {
|
||||
|
||||
private static final DateTimeFormatter isoDateFormatter = DateTimeFormatter.ofPattern(
|
||||
"yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]").withZone(ZoneId.systemDefault());
|
||||
|
||||
private static final DateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
|
||||
public TbDate() {
|
||||
super();
|
||||
}
|
||||
|
||||
public TbDate(String s) {
|
||||
super(parse(s));
|
||||
}
|
||||
|
||||
public TbDate(long date) {
|
||||
super(date);
|
||||
}
|
||||
|
||||
public TbDate(int year, int month, int date) {
|
||||
this(year, month, date, 0, 0, 0);
|
||||
}
|
||||
|
||||
public TbDate(int year, int month, int date, int hrs, int min) {
|
||||
this(year, month, date, hrs, min, 0);
|
||||
}
|
||||
|
||||
public TbDate(int year, int month, int date,
|
||||
int hrs, int min, int second) {
|
||||
super(new GregorianCalendar(year, month, date, hrs, min, second).getTimeInMillis());
|
||||
}
|
||||
|
||||
public String toDateString() {
|
||||
DateFormat formatter = DateFormat.getDateInstance();
|
||||
return formatter.format(this);
|
||||
}
|
||||
|
||||
public String toTimeString() {
|
||||
DateFormat formatter = DateFormat.getTimeInstance(DateFormat.LONG);
|
||||
return formatter.format(this);
|
||||
}
|
||||
|
||||
public String toISOString() {
|
||||
return isoDateFormat.format(this);
|
||||
}
|
||||
|
||||
public String toLocaleString(String locale) {
|
||||
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale));
|
||||
return formatter.format(this);
|
||||
}
|
||||
|
||||
public String toLocaleString(String locale, String tz) {
|
||||
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale));
|
||||
formatter.setTimeZone(TimeZone.getTimeZone(tz));
|
||||
return formatter.format(this);
|
||||
}
|
||||
|
||||
public static long now() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public static long parse(String value, String format) {
|
||||
try {
|
||||
DateFormat dateFormat = new SimpleDateFormat(format);
|
||||
return dateFormat.parse(value).getTime();
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static long parse(String value) {
|
||||
try {
|
||||
TemporalAccessor accessor = isoDateFormatter.parseBest(value,
|
||||
ZonedDateTime::from,
|
||||
LocalDateTime::from,
|
||||
LocalDate::from);
|
||||
Instant instant = Instant.from(accessor);
|
||||
return Instant.EPOCH.until(instant, ChronoUnit.MILLIS);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
return Date.parse(value);
|
||||
} catch (IllegalArgumentException e2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static long UTC(int year, int month, int date,
|
||||
int hrs, int min, int sec) {
|
||||
return Date.UTC(year - 1900, month, date, hrs, min, sec);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.mvel2.ExecutionContext;
|
||||
import org.mvel2.util.ArgsRepackUtil;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TbJson {
|
||||
|
||||
public static String stringify(Object value) {
|
||||
return value != null ? JacksonUtil.toString(value) : "null";
|
||||
}
|
||||
|
||||
public static Object parse(ExecutionContext ctx, String value) throws IOException {
|
||||
if (value != null) {
|
||||
JsonNode node = JacksonUtil.toJsonNode(value);
|
||||
if (node.isObject()) {
|
||||
return ArgsRepackUtil.repack(ctx, JacksonUtil.convertValue(node, Map.class));
|
||||
} else if (node.isArray()) {
|
||||
return ArgsRepackUtil.repack(ctx, JacksonUtil.convertValue(node, List.class));
|
||||
} else if (node.isDouble()) {
|
||||
return node.doubleValue();
|
||||
} else if (node.isLong()) {
|
||||
return node.longValue();
|
||||
} else if (node.isInt()) {
|
||||
return node.intValue();
|
||||
} else if (node.isBoolean()) {
|
||||
return node.booleanValue();
|
||||
} else if (node.isTextual()) {
|
||||
return node.asText();
|
||||
} else if (node.isBinary()) {
|
||||
return node.binaryValue();
|
||||
} else if (node.isNull()) {
|
||||
return null;
|
||||
} else {
|
||||
return node.asText();
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import org.mvel2.ExecutionContext;
|
||||
import org.mvel2.ParserConfiguration;
|
||||
import org.mvel2.execution.ExecutionArrayList;
|
||||
import org.mvel2.execution.ExecutionHashMap;
|
||||
import org.mvel2.util.MethodStub;
|
||||
import org.thingsboard.server.common.data.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class TbUtils {
|
||||
|
||||
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
public static void register(ParserConfiguration parserConfig) throws Exception {
|
||||
parserConfig.addImport("btoa", new MethodStub(TbUtils.class.getMethod("btoa",
|
||||
String.class)));
|
||||
parserConfig.addImport("atob", new MethodStub(TbUtils.class.getMethod("atob",
|
||||
String.class)));
|
||||
parserConfig.addImport("bytesToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
|
||||
List.class)));
|
||||
parserConfig.addImport("bytesToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
|
||||
List.class, String.class)));
|
||||
parserConfig.addImport("decodeToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
|
||||
List.class)));
|
||||
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
|
||||
ExecutionContext.class, List.class)));
|
||||
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
|
||||
ExecutionContext.class, String.class)));
|
||||
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
|
||||
ExecutionContext.class, String.class, String.class)));
|
||||
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseInt", new MethodStub(TbUtils.class.getMethod("parseInt",
|
||||
String.class, int.class)));
|
||||
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseBigEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToInt",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
|
||||
String.class)));
|
||||
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
|
||||
String.class, boolean.class)));
|
||||
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
|
||||
List.class, int.class, int.class)));
|
||||
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
|
||||
List.class, int.class, int.class, boolean.class)));
|
||||
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
|
||||
byte[].class, int.class, int.class)));
|
||||
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
|
||||
byte[].class, int.class, int.class, boolean.class)));
|
||||
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
|
||||
double.class, int.class)));
|
||||
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
|
||||
ExecutionContext.class, String.class)));
|
||||
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
|
||||
String.class)));
|
||||
parserConfig.addImport("base64ToBytes", new MethodStub(TbUtils.class.getMethod("base64ToBytes",
|
||||
String.class)));
|
||||
parserConfig.addImport("bytesToBase64", new MethodStub(TbUtils.class.getMethod("bytesToBase64",
|
||||
byte[].class)));
|
||||
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
|
||||
byte[].class)));
|
||||
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
|
||||
ExecutionArrayList.class)));
|
||||
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
|
||||
ExecutionContext.class, Map.class)));
|
||||
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
|
||||
ExecutionContext.class, Map.class, boolean.class)));
|
||||
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
|
||||
ExecutionContext.class, Map.class, List.class)));
|
||||
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
|
||||
ExecutionContext.class, Map.class, List.class, boolean.class)));
|
||||
}
|
||||
|
||||
public static String btoa(String input) {
|
||||
return new String(Base64.getEncoder().encode(input.getBytes()));
|
||||
}
|
||||
|
||||
public static String atob(String encoded) {
|
||||
return new String(Base64.getDecoder().decode(encoded));
|
||||
}
|
||||
|
||||
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
|
||||
return TbJson.parse(ctx, bytesToString(bytesList));
|
||||
}
|
||||
|
||||
public static String bytesToString(List<Byte> bytesList) {
|
||||
byte[] bytes = bytesFromList(bytesList);
|
||||
return new String(bytes);
|
||||
}
|
||||
|
||||
public static String bytesToString(List<Byte> bytesList, String charsetName) throws UnsupportedEncodingException {
|
||||
byte[] bytes = bytesFromList(bytesList);
|
||||
return new String(bytes, charsetName);
|
||||
}
|
||||
|
||||
public static List<Byte> stringToBytes(ExecutionContext ctx, String str) {
|
||||
byte[] bytes = str.getBytes();
|
||||
return bytesToList(ctx, bytes);
|
||||
}
|
||||
|
||||
public static List<Byte> stringToBytes(ExecutionContext ctx, String str, String charsetName) throws UnsupportedEncodingException {
|
||||
byte[] bytes = str.getBytes(charsetName);
|
||||
return bytesToList(ctx, bytes);
|
||||
}
|
||||
|
||||
private static byte[] bytesFromList(List<Byte> bytesList) {
|
||||
byte[] bytes = new byte[bytesList.size()];
|
||||
for (int i = 0; i < bytesList.size(); i++) {
|
||||
bytes[i] = bytesList.get(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static List<Byte> bytesToList(ExecutionContext ctx, byte[] bytes) {
|
||||
List<Byte> list = new ExecutionArrayList<>(ctx);
|
||||
for (byte aByte : bytes) {
|
||||
list.add(aByte);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static Integer parseInt(String value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
int radix = 10;
|
||||
if (isHexadecimal(value)) {
|
||||
radix = 16;
|
||||
}
|
||||
return Integer.parseInt(prepareNumberString(value), radix);
|
||||
} catch (NumberFormatException e) {
|
||||
Float f = parseFloat(value);
|
||||
if (f != null) {
|
||||
return f.intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Integer parseInt(String value, int radix) {
|
||||
if (value != null) {
|
||||
try {
|
||||
return Integer.parseInt(prepareNumberString(value), radix);
|
||||
} catch (NumberFormatException e) {
|
||||
Float f = parseFloat(value);
|
||||
if (f != null) {
|
||||
return f.intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Float parseFloat(String value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
return Float.parseFloat(prepareNumberString(value));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Double parseDouble(String value) {
|
||||
if (value != null) {
|
||||
try {
|
||||
return Double.parseDouble(prepareNumberString(value));
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int parseLittleEndianHexToInt(String hex) {
|
||||
return parseHexToInt(hex, false);
|
||||
}
|
||||
|
||||
public static int parseBigEndianHexToInt(String hex) {
|
||||
return parseHexToInt(hex, true);
|
||||
}
|
||||
|
||||
public static int parseHexToInt(String hex) {
|
||||
return parseHexToInt(hex, true);
|
||||
}
|
||||
|
||||
public static int parseHexToInt(String hex, boolean bigEndian) {
|
||||
int length = hex.length();
|
||||
if (length > 8) {
|
||||
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
|
||||
}
|
||||
if (length % 2 > 0) {
|
||||
throw new IllegalArgumentException("Hex string must be even-length.");
|
||||
}
|
||||
byte[] data = new byte[length / 2];
|
||||
for (int i = 0; i < length; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return parseBytesToInt(data, 0, data.length, bigEndian);
|
||||
}
|
||||
|
||||
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String hex) {
|
||||
int len = hex.length();
|
||||
if (len % 2 > 0) {
|
||||
throw new IllegalArgumentException("Hex string must be even-length.");
|
||||
}
|
||||
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||||
+ Character.digit(hex.charAt(i + 1), 16)));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public static String base64ToHex(String base64) {
|
||||
return bytesToHex(Base64.getDecoder().decode(base64));
|
||||
}
|
||||
|
||||
public static String bytesToBase64(byte[] bytes) {
|
||||
return Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
public static byte[] base64ToBytes(String input) {
|
||||
return Base64.getDecoder().decode(input);
|
||||
}
|
||||
|
||||
public static int parseBytesToInt(List<Byte> data, int offset, int length) {
|
||||
return parseBytesToInt(data, offset, length, true);
|
||||
}
|
||||
|
||||
public static int parseBytesToInt(List<Byte> data, int offset, int length, boolean bigEndian) {
|
||||
final byte[] bytes = new byte[data.size()];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = data.get(i);
|
||||
}
|
||||
return parseBytesToInt(bytes, offset, length, bigEndian);
|
||||
}
|
||||
|
||||
public static int parseBytesToInt(byte[] data, int offset, int length) {
|
||||
return parseBytesToInt(data, offset, length, true);
|
||||
}
|
||||
|
||||
public static int parseBytesToInt(byte[] data, int offset, int length, boolean bigEndian) {
|
||||
if (offset > data.length) {
|
||||
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
|
||||
}
|
||||
if (length > 4) {
|
||||
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!");
|
||||
}
|
||||
if (offset + length > data.length) {
|
||||
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
|
||||
}
|
||||
var bb = ByteBuffer.allocate(4);
|
||||
if (!bigEndian) {
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
bb.position(bigEndian ? 4 - length : 0);
|
||||
bb.put(data, offset, length);
|
||||
bb.position(0);
|
||||
return bb.getInt();
|
||||
}
|
||||
|
||||
public static String bytesToHex(ExecutionArrayList<?> bytesList) {
|
||||
byte[] bytes = new byte[bytesList.size()];
|
||||
for (int i = 0; i < bytesList.size(); i++) {
|
||||
bytes[i] = Byte.parseByte(bytesList.get(i).toString());
|
||||
}
|
||||
return bytesToHex(bytes);
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
byte[] hexChars = new byte[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static double toFixed(double value, int precision) {
|
||||
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
|
||||
private static boolean isHexadecimal(String value) {
|
||||
return value != null && (value.contains("0x") || value.contains("0X"));
|
||||
}
|
||||
|
||||
private static String prepareNumberString(String value) {
|
||||
if (value != null) {
|
||||
value = value.trim();
|
||||
if (isHexadecimal(value)) {
|
||||
value = value.replace("0x", "");
|
||||
value = value.replace("0X", "");
|
||||
}
|
||||
value = value.replace(",", ".");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) {
|
||||
return toFlatMap(ctx, json, new ArrayList<>(), true);
|
||||
}
|
||||
|
||||
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, boolean pathInKey) {
|
||||
return toFlatMap(ctx, json, new ArrayList<>(), pathInKey);
|
||||
}
|
||||
|
||||
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, List<String> excludeList) {
|
||||
return toFlatMap(ctx, json, excludeList, true);
|
||||
}
|
||||
|
||||
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, List<String> excludeList, boolean pathInKey) {
|
||||
ExecutionHashMap<String, Object> map = new ExecutionHashMap<>(16, ctx);
|
||||
parseRecursive(json, map, excludeList, "", pathInKey);
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
|
||||
if (json instanceof Map.Entry) {
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) json;
|
||||
if (StringUtils.isNotBlank(path)) {
|
||||
path += ".";
|
||||
}
|
||||
if (excludeList.contains(entry.getKey())) {
|
||||
return;
|
||||
}
|
||||
path += entry.getKey();
|
||||
json = entry.getValue();
|
||||
}
|
||||
if (json instanceof Set || json instanceof List) {
|
||||
String arrayPath = path + ".";
|
||||
Object[] collection = ((Collection<?>) json).toArray();
|
||||
for (int index = 0; index < collection.length; index++) {
|
||||
parseRecursive(collection[index], map, excludeList, arrayPath + index, pathInKey);
|
||||
}
|
||||
} else if (json instanceof Map) {
|
||||
Map<?, ?> node = (Map<?, ?>) json;
|
||||
for (Map.Entry<?, ?> entry : node.entrySet()) {
|
||||
parseRecursive(entry, map, excludeList, path, pathInKey);
|
||||
}
|
||||
} else {
|
||||
if (pathInKey) {
|
||||
map.put(path, json);
|
||||
} else {
|
||||
String key = path.substring(path.lastIndexOf('.') + 1);
|
||||
if (StringUtils.isNumeric(key)) {
|
||||
int pos = path.length();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
pos = path.lastIndexOf('.', pos - 1);
|
||||
}
|
||||
key = path.substring(pos + 1);
|
||||
}
|
||||
map.put(key, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import org.thingsboard.script.api.ScriptInvokeService;
|
||||
import org.thingsboard.server.common.data.script.ScriptLanguage;
|
||||
|
||||
public interface TbelInvokeService extends ScriptInvokeService {
|
||||
|
||||
@Override
|
||||
default ScriptLanguage getLanguage() {
|
||||
return ScriptLanguage.TBEL;
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class TbelScript {
|
||||
|
||||
private final String scriptBody;
|
||||
private final String[] argNames;
|
||||
|
||||
public Map createVars(Object[] args) {
|
||||
if (args == null || args.length != argNames.length) {
|
||||
throw new IllegalArgumentException("Invalid number of argument values");
|
||||
}
|
||||
var result = new HashMap<>();
|
||||
for (int i = 0; i < argNames.length; i++) {
|
||||
result.put(argNames[i], args[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.mvel2.ExecutionContext;
|
||||
import org.thingsboard.script.api.TbScriptExecutionTask;
|
||||
|
||||
|
||||
public class TbelScriptExecutionTask extends TbScriptExecutionTask {
|
||||
|
||||
private final ExecutionContext context;
|
||||
|
||||
public TbelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) {
|
||||
super(resultFuture);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(){
|
||||
context.stop();
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright © 2016-2023 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.tbel;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mvel2.ExecutionContext;
|
||||
import org.mvel2.ParserContext;
|
||||
import org.mvel2.SandboxedParserConfiguration;
|
||||
import org.mvel2.execution.ExecutionArrayList;
|
||||
import org.mvel2.execution.ExecutionHashMap;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
@Slf4j
|
||||
|
||||
public class TbUtilsTest {
|
||||
|
||||
private ExecutionContext ctx;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode();
|
||||
parserConfig.addImport("JSON", TbJson.class);
|
||||
parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
|
||||
parserConfig.registerDataType("Random", Random.class, date -> 8L);
|
||||
parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L);
|
||||
try {
|
||||
TbUtils.register(parserConfig);
|
||||
} catch (Exception e) {
|
||||
log.error("Cannot register functions", e);
|
||||
}
|
||||
ctx = new ExecutionContext(parserConfig);
|
||||
Assert.assertNotNull(ctx);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
ctx.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHexToInt() {
|
||||
Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB"));
|
||||
Assert.assertEquals(0xABBA, TbUtils.parseHexToInt("ABBA", true));
|
||||
Assert.assertEquals(0xBAAB, TbUtils.parseHexToInt("ABBA", false));
|
||||
Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("AABBCC", true));
|
||||
Assert.assertEquals(0xAABBCC, TbUtils.parseHexToInt("CCBBAA", false));
|
||||
Assert.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("AABBCCDD", true));
|
||||
Assert.assertEquals(0xAABBCCDD, TbUtils.parseHexToInt("DDCCBBAA", false));
|
||||
Assert.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("DDCCBBAA", true));
|
||||
Assert.assertEquals(0xDDCCBBAA, TbUtils.parseHexToInt("AABBCCDD", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseBytesToInt_checkPrimitives() {
|
||||
int expected = 257;
|
||||
byte[] data = ByteBuffer.allocate(4).putInt(expected).array();
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4));
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 2, 2, true));
|
||||
Assert.assertEquals(1, TbUtils.parseBytesToInt(data, 3, 1, true));
|
||||
|
||||
expected = Integer.MAX_VALUE;
|
||||
data = ByteBuffer.allocate(4).putInt(expected).array();
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, true));
|
||||
|
||||
expected = 0xAABBCCDD;
|
||||
data = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD};
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, true));
|
||||
data = new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA};
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, false));
|
||||
|
||||
expected = 0xAABBCC;
|
||||
data = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC};
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, true));
|
||||
data = new byte[]{(byte) 0xCC, (byte) 0xBB, (byte) 0xAA};
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseBytesToInt_checkLists() {
|
||||
int expected = 257;
|
||||
List<Byte> data = toList(ByteBuffer.allocate(4).putInt(expected).array());
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4));
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 2, 2, true));
|
||||
Assert.assertEquals(1, TbUtils.parseBytesToInt(data, 3, 1, true));
|
||||
|
||||
expected = Integer.MAX_VALUE;
|
||||
data = toList(ByteBuffer.allocate(4).putInt(expected).array());
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, true));
|
||||
|
||||
expected = 0xAABBCCDD;
|
||||
data = toList(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD});
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, true));
|
||||
data = toList(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA});
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 4, false));
|
||||
|
||||
expected = 0xAABBCC;
|
||||
data = toList(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC});
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, true));
|
||||
data = toList(new byte[]{(byte) 0xCC, (byte) 0xBB, (byte) 0xAA});
|
||||
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toFlatMap() {
|
||||
ExecutionHashMap<String, Object> inputMap = new ExecutionHashMap<>(16, ctx);
|
||||
inputMap.put("name", "Alice");
|
||||
inputMap.put("age", 30);
|
||||
inputMap.put("devices", new ExecutionArrayList<>(List.of(
|
||||
new ExecutionHashMap<>(16, ctx) {{
|
||||
put("id", "dev001");
|
||||
put("type", "sensor");
|
||||
}},
|
||||
new ExecutionHashMap<>(16, ctx) {{
|
||||
put("id", "dev002");
|
||||
put("type", "actuator");
|
||||
}}
|
||||
), ctx));
|
||||
inputMap.put("settings", new ExecutionHashMap<>(16, ctx) {{
|
||||
put("notifications", true);
|
||||
put("timezone", "UTC-5");
|
||||
put("params", new ExecutionHashMap<>(16, ctx) {{
|
||||
put("param1", "value1");
|
||||
put("param2", "value2");
|
||||
put("param3", new ExecutionHashMap<>(16, ctx) {{
|
||||
put("subParam1", "value1");
|
||||
put("subParam2", "value2");
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
ExecutionArrayList<String> excludeList = new ExecutionArrayList<>(ctx);
|
||||
excludeList.addAll(List.of("age", "id", "param1", "subParam2"));
|
||||
|
||||
ExecutionHashMap<String, Object> expectedMapWithPath = new ExecutionHashMap<>(16, ctx);
|
||||
expectedMapWithPath.put("name", "Alice");
|
||||
expectedMapWithPath.put("devices.0.type", "sensor");
|
||||
expectedMapWithPath.put("devices.1.type", "actuator");
|
||||
expectedMapWithPath.put("settings.notifications", true);
|
||||
expectedMapWithPath.put("settings.timezone", "UTC-5");
|
||||
expectedMapWithPath.put("settings.params.param2", "value2");
|
||||
expectedMapWithPath.put("settings.params.param3.subParam1", "value1");
|
||||
|
||||
ExecutionHashMap<String, Object> actualMapWithPaths = TbUtils.toFlatMap(ctx, inputMap, excludeList, true);
|
||||
|
||||
Assert.assertEquals(expectedMapWithPath, actualMapWithPaths);
|
||||
|
||||
ExecutionHashMap<String, Object> expectedMapWithoutPaths = new ExecutionHashMap<>(16, ctx);
|
||||
expectedMapWithoutPaths.put("timezone", "UTC-5");
|
||||
expectedMapWithoutPaths.put("name", "Alice");
|
||||
expectedMapWithoutPaths.put("id", "dev002");
|
||||
expectedMapWithoutPaths.put("subParam2", "value2");
|
||||
expectedMapWithoutPaths.put("type", "actuator");
|
||||
expectedMapWithoutPaths.put("subParam1", "value1");
|
||||
expectedMapWithoutPaths.put("param1", "value1");
|
||||
expectedMapWithoutPaths.put("notifications", true);
|
||||
expectedMapWithoutPaths.put("age", 30);
|
||||
expectedMapWithoutPaths.put("param2", "value2");
|
||||
|
||||
ExecutionHashMap<String, Object> actualMapWithoutPaths = TbUtils.toFlatMap(ctx, inputMap, false);
|
||||
|
||||
Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths);
|
||||
}
|
||||
|
||||
|
||||
private static String keyToValue(String key, String extraSymbol) {
|
||||
return key + "Value" + (extraSymbol == null ? "" : extraSymbol);
|
||||
}
|
||||
|
||||
private static List<Byte> toList(byte[] data) {
|
||||
List<Byte> result = new ArrayList<>(data.length);
|
||||
for (Byte b : data) {
|
||||
result.add(b);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user