first-fix

This commit is contained in:
Alexander Abakumov committed 2024-06-23 23:04:49 +03:00
1 parent 6d87c1edc8
commit 565d9d673a
6284 files changed
+820267 -76

No files matched your search

+124
View File
@@ -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>
@@ -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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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
}
@@ -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;
}
}
@@ -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();
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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
}
}
@@ -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;
}
@@ -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;");
}
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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;
}
}