Browse Source

Merge remote-tracking branch 'origin/master' into feature/edge-limit-attempt

pull/7478/head
Volodymyr Babak 4 years ago
parent
commit
19287fa250
  1. 10
      application/pom.xml
  2. 8
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  3. 8
      application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json
  4. 8
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  5. 2
      application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
  6. 16
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  7. 135
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  8. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  9. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  10. 20
      application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
  11. 22
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  12. 41
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  13. 23
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  14. 5
      application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java
  15. 7
      application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  17. 7
      application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
  18. 6
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  19. 249
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  20. 185
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java
  21. 64
      application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java
  22. 175
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  23. 171
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java
  24. 133
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java
  25. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java
  26. 9
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  27. 6
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  28. 7
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  29. 4
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  30. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  31. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  32. 27
      application/src/main/resources/thingsboard.yml
  33. 6
      application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java
  34. 128
      application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java
  35. 38
      application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java
  36. 34
      application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java
  37. 196
      application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java
  38. 24
      application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java
  39. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  41. 15
      common/data/src/main/java/org/thingsboard/server/common/data/HasRuleEngineProfile.java
  42. 10
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  43. 3
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java
  44. 20
      common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java
  45. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java
  46. 30
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettingsInfo.java
  47. 7
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
  48. 1
      common/pom.xml
  49. 7
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  50. 42
      common/script/pom.xml
  51. 89
      common/script/remote-js-client/pom.xml
  52. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
  53. 218
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  54. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
  55. 2
      common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
  56. 121
      common/script/script-api/pom.xml
  57. 288
      common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java
  58. 44
      common/script/script-api/src/main/java/org/thingsboard/script/api/BlockedScriptInfo.java
  59. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/RuleNodeScriptFactory.java
  60. 11
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptInvokeService.java
  61. 19
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java
  62. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptType.java
  63. 40
      common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java
  64. 30
      common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptExecutionTask.java
  65. 106
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java
  66. 28
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java
  67. 31
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsScriptExecutionTask.java
  68. 15
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsScriptInfo.java
  69. 179
      common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java
  70. 181
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java
  71. 28
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java
  72. 41
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java
  73. 38
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScriptExecutionTask.java
  74. 61
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbJson.java
  75. 87
      common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbUtils.java
  76. 4
      common/stats/src/main/java/org/thingsboard/server/common/stats/TbApiUsageReportClient.java
  77. 25
      common/stats/src/main/java/org/thingsboard/server/common/stats/TbApiUsageStateClient.java
  78. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  79. 34
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  80. 7
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java
  81. 182
      common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java
  82. 45
      dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java
  83. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
  84. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  85. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java
  86. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
  87. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  88. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java
  89. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java
  90. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
  91. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  92. 2
      dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java
  93. 10
      dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
  94. 8
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  95. 18
      pom.xml
  96. 18
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  97. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  98. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  99. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  100. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

10
application/pom.xml

@ -110,7 +110,15 @@
<artifactId>queue</artifactId> <artifactId>queue</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common.script</groupId>
<artifactId>script-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common.script</groupId>
<artifactId>remote-js-client</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>stats</artifactId> <artifactId>stats</artifactId>
</dependency> </dependency>
<dependency> <dependency>

8
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -57,7 +57,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -69,7 +71,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {

8
application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json

@ -70,7 +70,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -82,7 +84,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {

8
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -57,7 +57,9 @@
"name": "Log RPC from Device", "name": "Log RPC from Device",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {
@ -69,7 +71,9 @@
"name": "Log Other", "name": "Log Other",
"debugMode": false, "debugMode": false,
"configuration": { "configuration": {
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" "scriptLang": "MVEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
} }
}, },
{ {

2
application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java

@ -26,7 +26,7 @@ import java.util.Arrays;
@SpringBootConfiguration @SpringBootConfiguration
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
@ComponentScan({"org.thingsboard.server"}) @ComponentScan({"org.thingsboard.server", "org.thingsboard.script"})
public class ThingsboardServerApplication { public class ThingsboardServerApplication {
private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";

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

@ -15,9 +15,7 @@
*/ */
package org.thingsboard.server.actors; package org.thingsboard.server.actors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -34,6 +32,8 @@ import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
@ -77,7 +78,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
@ -92,7 +92,6 @@ import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRpcService; import org.thingsboard.server.service.rpc.TbRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.script.JsInvokeService;
import org.thingsboard.server.service.session.DeviceSessionCacheService; import org.thingsboard.server.service.session.DeviceSessionCacheService;
import org.thingsboard.server.service.sms.SmsExecutorService; import org.thingsboard.server.service.sms.SmsExecutorService;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;
@ -105,7 +104,6 @@ import javax.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -152,7 +150,7 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private TbApiUsageClient apiUsageClient; private TbApiUsageReportClient apiUsageClient;
@Autowired @Autowired
@Getter @Getter
@ -270,7 +268,11 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private JsInvokeService jsSandbox; private JsInvokeService jsInvokeService;
@Autowired(required = false)
@Getter
private MvelInvokeService mvelInvokeService;
@Autowired @Autowired
@Getter @Getter

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

@ -17,8 +17,12 @@ package org.thingsboard.server.actors.ruleChain;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.Arrays;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
@ -39,6 +43,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
@ -52,10 +57,12 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -86,8 +93,10 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -330,58 +339,62 @@ class DefaultTbContext implements TbContext {
} }
public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) { public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) {
RuleChainId ruleChainId = null; DeviceProfile deviceProfile = null;
String queueName = null;
if (device.getDeviceProfileId() != null) { if (device.getDeviceProfileId() != null) {
DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId()); deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId());
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", device.getDeviceProfileId());
} else {
ruleChainId = deviceProfile.getDefaultRuleChainId();
queueName = deviceProfile.getDefaultQueueName();
}
} }
return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueName, ruleChainId); return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, deviceProfile);
} }
public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) { public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) {
RuleChainId ruleChainId = null; AssetProfile assetProfile = null;
String queueName = null;
if (asset.getAssetProfileId() != null) { if (asset.getAssetProfileId() != null) {
AssetProfile assetProfile = mainCtx.getAssetProfileCache().find(asset.getAssetProfileId()); assetProfile = mainCtx.getAssetProfileCache().find(asset.getAssetProfileId());
if (assetProfile == null) {
log.warn("[{}] Asset profile is null!", asset.getAssetProfileId());
} else {
ruleChainId = assetProfile.getDefaultRuleChainId();
queueName = assetProfile.getDefaultQueueName();
}
} }
return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueName, ruleChainId); return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, assetProfile);
} }
public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) { public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) {
RuleChainId ruleChainId = null; HasRuleEngineProfile profile = null;
String queueName = null;
if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) { if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) {
DeviceId deviceId = new DeviceId(alarm.getOriginator().getId()); DeviceId deviceId = new DeviceId(alarm.getOriginator().getId());
DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId);
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", deviceId);
} else {
ruleChainId = deviceProfile.getDefaultRuleChainId();
queueName = deviceProfile.getDefaultQueueName();
}
} else if (EntityType.ASSET.equals(alarm.getOriginator().getEntityType())) { } else if (EntityType.ASSET.equals(alarm.getOriginator().getEntityType())) {
AssetId assetId = new AssetId(alarm.getOriginator().getId()); AssetId assetId = new AssetId(alarm.getOriginator().getId());
AssetProfile assetProfile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId); profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId);
if (assetProfile == null) { }
log.warn("[{}] Asset profile is null!", assetId); return entityActionMsg(alarm, alarm.getOriginator(), ruleNodeId, action, profile);
} else { }
ruleChainId = assetProfile.getDefaultRuleChainId();
queueName = assetProfile.getDefaultQueueName(); public TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<AttributeKvEntry> attributes) {
} ObjectNode entityNode = JacksonUtil.newObjectNode();
if (attributes != null) {
attributes.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry));
} }
return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId); return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode));
}
public TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<String> keys) {
ObjectNode entityNode = JacksonUtil.newObjectNode();
ArrayNode attrsArrayNode = entityNode.putArray("attributes");
if (keys != null) {
keys.forEach(attrsArrayNode::add);
}
return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode));
}
private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, String action, String msgData) {
TbMsgMetaData tbMsgMetaData = getActionMetaData(ruleNodeId);
tbMsgMetaData.putValue("scope", scope);
HasRuleEngineProfile profile = null;
if (EntityType.DEVICE.equals(originator.getEntityType())) {
DeviceId deviceId = new DeviceId(originator.getId());
profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId);
} else if (EntityType.ASSET.equals(originator.getEntityType())) {
AssetId assetId = new AssetId(originator.getId());
profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId);
}
return entityActionMsg(originator, tbMsgMetaData, msgData, action, profile);
} }
@Override @Override
@ -390,17 +403,27 @@ class DefaultTbContext implements TbContext {
} }
public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) {
return entityActionMsg(entity, id, ruleNodeId, action, null, null); return entityActionMsg(entity, id, ruleNodeId, action, null);
} }
public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, String queueName, RuleChainId ruleChainId) { public <E, I extends EntityId, K extends HasRuleEngineProfile> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, K profile) {
try { try {
return TbMsg.newMsg(queueName, action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), ruleChainId, null); return entityActionMsg(id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), action, profile);
} catch (JsonProcessingException | IllegalArgumentException e) { } catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e); throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e);
} }
} }
private <I extends EntityId, K extends HasRuleEngineProfile> TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, String action, K profile) {
String defaultQueueName = null;
RuleChainId defaultRuleChainId = null;
if (profile != null) {
defaultQueueName = profile.getDefaultQueueName();
defaultRuleChainId = profile.getDefaultRuleChainId();
}
return TbMsg.newMsg(defaultQueueName, action, id, msgMetaData, msgData, defaultRuleChainId, null);
}
@Override @Override
public RuleNodeId getSelfId() { public RuleNodeId getSelfId() {
return nodeCtx.getSelf().getId(); return nodeCtx.getSelf().getId();
@ -442,8 +465,38 @@ class DefaultTbContext implements TbContext {
} }
@Override @Override
@Deprecated
public ScriptEngine createJsScriptEngine(String script, String... argNames) { public ScriptEngine createJsScriptEngine(String script, String... argNames) {
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames); return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames);
}
private ScriptEngine createMvelScriptEngine(String script, String... argNames) {
if (mainCtx.getMvelInvokeService() == null) {
throw new RuntimeException("MVEL execution is disabled!");
}
return new RuleNodeMvelScriptEngine(getTenantId(), mainCtx.getMvelInvokeService(), script, argNames);
}
@Override
public ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script, String... argNames) {
if (scriptLang == null) {
scriptLang = ScriptLanguage.JS;
}
if (StringUtils.isBlank(script)) {
throw new RuntimeException(scriptLang.name() + " script is blank!");
}
switch (scriptLang) {
case JS:
return createJsScriptEngine(script, argNames);
case MVEL:
if (Arrays.isNullOrEmpty(argNames)) {
return createMvelScriptEngine(script, "msg", "metadata", "msgType");
} else {
return createMvelScriptEngine(script, argNames);
}
default:
throw new RuntimeException("Unsupported script language: " + scriptLang.name());
}
} }
@Override @Override

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -43,12 +43,12 @@ import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.common.MultipleTbQueueTbMsgCallbackWrapper; import org.thingsboard.server.queue.common.MultipleTbQueueTbMsgCallbackWrapper;
import org.thingsboard.server.queue.common.TbQueueTbMsgCallbackWrapper; import org.thingsboard.server.queue.common.TbQueueTbMsgCallbackWrapper;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import java.util.ArrayList; import java.util.ArrayList;
@ -73,7 +73,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes; private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
private final RuleChainService service; private final RuleChainService service;
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private String ruleChainName; private String ruleChainName;
private RuleNodeId firstId; private RuleNodeId firstId;

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -31,7 +31,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -40,7 +40,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
private final String ruleChainName; private final String ruleChainName;
private final TbActorRef self; private final TbActorRef self;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private DefaultTbContext defaultCtx; private DefaultTbContext defaultCtx;

20
application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java

@ -22,7 +22,7 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -35,8 +35,8 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
@ -45,7 +45,7 @@ import java.util.concurrent.ConcurrentMap;
@Slf4j @Slf4j
@Component @Component
public class RateLimitProcessingFilter extends GenericFilterBean { public class RateLimitProcessingFilter extends OncePerRequestFilter {
@Autowired @Autowired
private ThingsboardErrorResponseHandler errorResponseHandler; private ThingsboardErrorResponseHandler errorResponseHandler;
@ -58,13 +58,13 @@ public class RateLimitProcessingFilter extends GenericFilterBean {
private final ConcurrentMap<CustomerId, TbRateLimits> perCustomerLimits = new ConcurrentHashMap<>(); private final ConcurrentMap<CustomerId, TbRateLimits> perCustomerLimits = new ConcurrentHashMap<>();
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) { if (user != null && !user.isSystemAdmin()) {
var profile = tenantProfileCache.get(user.getTenantId()); var profile = tenantProfileCache.get(user.getTenantId());
if (profile == null) { if (profile == null) {
log.debug("[{}] Failed to lookup tenant profile", user.getTenantId()); log.debug("[{}] Failed to lookup tenant profile", user.getTenantId());
errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), (HttpServletResponse) response); errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), response);
return; return;
} }
var profileConfiguration = profile.getDefaultProfileConfiguration(); var profileConfiguration = profile.getDefaultProfileConfiguration();
@ -80,6 +80,16 @@ public class RateLimitProcessingFilter extends GenericFilterBean {
chain.doFilter(request, response); chain.doFilter(request, response);
} }
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected boolean shouldNotFilterErrorDispatch() {
return false;
}
private <I extends EntityId> boolean checkRateLimits(I ownerId, String rateLimitConfig, Map<I, TbRateLimits> rateLimitsMap, ServletResponse response) { private <I extends EntityId> boolean checkRateLimits(I ownerId, String rateLimitConfig, Map<I, TbRateLimits> rateLimitsMap, ServletResponse response) {
if (StringUtils.isNotEmpty(rateLimitConfig)) { if (StringUtils.isNotEmpty(rateLimitConfig)) {
TbRateLimits rateLimits = rateLimitsMap.get(ownerId); TbRateLimits rateLimits = rateLimitsMap.get(ownerId);

22
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest; import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings; import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
@ -195,7 +196,6 @@ public class AdminController extends BaseController {
notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings") @GetMapping("/repositorySettings")
@ResponseBody
public RepositorySettings getRepositorySettings() throws ThingsboardException { public RepositorySettings getRepositorySettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -213,7 +213,6 @@ public class AdminController extends BaseController {
notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/exists") @GetMapping("/repositorySettings/exists")
@ResponseBody
public Boolean repositorySettingsExists() throws ThingsboardException { public Boolean repositorySettingsExists() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -223,6 +222,23 @@ public class AdminController extends BaseController {
} }
} }
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/repositorySettings/info")
public RepositorySettingsInfo getRepositorySettingsInfo() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
RepositorySettings repositorySettings = versionControlService.getVersionControlSettings(getTenantId());
if (repositorySettings != null) {
return RepositorySettingsInfo.builder()
.configured(true)
.readOnly(repositorySettings.isReadOnly())
.build();
} else {
return RepositorySettingsInfo.builder()
.configured(false)
.build();
}
}
@ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)", @ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -274,7 +290,6 @@ public class AdminController extends BaseController {
notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings") @GetMapping("/autoCommitSettings")
@ResponseBody
public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException { public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
@ -288,7 +303,6 @@ public class AdminController extends BaseController {
notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/autoCommitSettings/exists") @GetMapping("/autoCommitSettings/exists")
@ResponseBody
public Boolean autoCommitSettingsExists() throws ThingsboardException { public Boolean autoCommitSettingsExists() throws ThingsboardException {
try { try {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);

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

@ -37,6 +37,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
@ -59,14 +61,15 @@ import org.thingsboard.server.common.data.rule.RuleChainImportResult;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.script.JsInvokeService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
@ -117,10 +120,10 @@ public class RuleChainController extends BaseController {
private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " + private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " +
"List of rule nodes and their connection is stored in a separate 'metadata' object."; "List of rule nodes and their connection is stored in a separate 'metadata' object.";
private static final String RULE_CHAIN_METADATA_DESCRIPTION = "The metadata object contains information about the rule nodes and their connections."; private static final String RULE_CHAIN_METADATA_DESCRIPTION = "The metadata object contains information about the rule nodes and their connections.";
private static final String TEST_JS_FUNCTION = "Execute the JavaScript function and return the result. The format of request: \n\n" private static final String TEST_SCRIPT_FUNCTION = "Execute the Script function and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ "{\n" + + "{\n" +
" \"script\": \"Your JS Function as String\",\n" + " \"script\": \"Your Function as String\",\n" +
" \"scriptType\": \"One of: update, generate, filter, switch, json, string\",\n" + " \"scriptType\": \"One of: update, generate, filter, switch, json, string\",\n" +
" \"argNames\": [\"msg\", \"metadata\", \"type\"],\n" + " \"argNames\": [\"msg\", \"metadata\", \"type\"],\n" +
" \"msg\": \"{\\\"temperature\\\": 42}\", \n" + " \"msg\": \"{\\\"temperature\\\": 42}\", \n" +
@ -142,12 +145,18 @@ public class RuleChainController extends BaseController {
@Autowired @Autowired
private JsInvokeService jsInvokeService; private JsInvokeService jsInvokeService;
@Autowired(required = false)
private MvelInvokeService mvelInvokeService;
@Autowired(required = false) @Autowired(required = false)
private ActorSystemContext actorContext; private ActorSystemContext actorContext;
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}") @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled; private boolean debugPerTenantEnabled;
@Value("${mvel.enabled:true}")
private boolean mvelEnabled;
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@ -369,13 +378,23 @@ public class RuleChainController extends BaseController {
} }
} }
@ApiOperation(value = "Is MVEL script executor enabled",
notes = "Returns 'True' if the MVEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/mvelEnabled", method = RequestMethod.GET)
@ResponseBody
public Boolean isMvelEnabled() {
return mvelEnabled;
}
@ApiOperation(value = "Test JavaScript function", @ApiOperation(value = "Test Script function",
notes = TEST_JS_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public JsonNode testScript( public JsonNode testScript(
@ApiParam(value = "Script language: JS or MVEL")
@RequestParam(required = false) ScriptLanguage scriptLang,
@ApiParam(value = "Test JS request. See API call description above.") @ApiParam(value = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException { @RequestBody JsonNode inputParams) throws ThingsboardException {
try { try {
@ -393,7 +412,17 @@ public class RuleChainController extends BaseController {
String errorText = ""; String errorText = "";
ScriptEngine engine = null; ScriptEngine engine = null;
try { try {
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, getCurrentUser().getId(), script, argNames); if (scriptLang == null) {
scriptLang = ScriptLanguage.JS;
}
if (ScriptLanguage.JS.equals(scriptLang)) {
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames);
} else {
if (mvelInvokeService == null) {
throw new IllegalArgumentException("MVEL script engine is disabled!");
}
engine = new RuleNodeMvelScriptEngine(getTenantId(), mvelInvokeService, script, argNames);
}
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) { switch (scriptType) {
case "update": case "update":

23
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -34,8 +35,6 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
@ -180,7 +179,7 @@ public class EntityActionService {
metaData.putValue(DataConstants.SCOPE, scope); metaData.putValue(DataConstants.SCOPE, scope);
if (attributes != null) { if (attributes != null) {
for (AttributeKvEntry attr : attributes) { for (AttributeKvEntry attr : attributes) {
addKvEntry(entityNode, attr); JacksonUtil.addKvEntry(entityNode, attr);
} }
} }
} else if (actionType == ActionType.ATTRIBUTES_DELETED) { } else if (actionType == ActionType.ATTRIBUTES_DELETED) {
@ -258,27 +257,11 @@ public class EntityActionService {
element.put("ts", entry.getKey()); element.put("ts", entry.getKey());
ObjectNode values = element.putObject("values"); ObjectNode values = element.putObject("values");
for (TsKvEntry tsKvEntry : entry.getValue()) { for (TsKvEntry tsKvEntry : entry.getValue()) {
addKvEntry(values, tsKvEntry); JacksonUtil.addKvEntry(values, tsKvEntry);
} }
result.add(element); result.add(element);
} }
} }
} }
private void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) throws Exception {
if (kvEntry.getDataType() == DataType.BOOLEAN) {
kvEntry.getBooleanValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.DOUBLE) {
kvEntry.getDoubleValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.LONG) {
kvEntry.getLongValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.JSON) {
if (kvEntry.getJsonValue().isPresent()) {
entityNode.set(kvEntry.getKey(), json.readTree(kvEntry.getJsonValue().get()));
}
} else {
entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
}
}
} }

5
application/src/main/java/org/thingsboard/server/service/apiusage/TbApiUsageStateService.java

@ -21,16 +21,15 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
public interface TbApiUsageStateService extends ApplicationListener<PartitionChangeEvent> { public interface TbApiUsageStateService extends TbApiUsageStateClient, ApplicationListener<PartitionChangeEvent> {
void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback); void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback);
ApiUsageState getApiUsageState(TenantId tenantId);
void onTenantProfileUpdate(TenantProfileId tenantProfileId); void onTenantProfileUpdate(TenantProfileId tenantProfileId);
void onTenantUpdate(TenantId tenantId); void onTenantUpdate(TenantId tenantId);

7
application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java

@ -20,19 +20,18 @@ import com.fasterxml.jackson.databind.node.TextNode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.asset.TbAssetService; import org.thingsboard.server.service.entitiy.asset.TbAssetService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -47,7 +46,7 @@ public class AssetBulkImportService extends AbstractBulkImportService<Asset> {
@Override @Override
protected void setEntityFields(Asset entity, Map<BulkImportColumnType, String> fields) { protected void setEntityFields(Asset entity, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity);
fields.forEach((columnType, value) -> { fields.forEach((columnType, value) -> {
switch (columnType) { switch (columnType) {
case NAME: case NAME:

2
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -76,7 +76,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
@Override @Override
protected void setEntityFields(Device entity, Map<BulkImportColumnType, String> fields) { protected void setEntityFields(Device entity, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity);
fields.forEach((columnType, value) -> { fields.forEach((columnType, value) -> {
switch (columnType) { switch (columnType) {
case NAME: case NAME:

7
application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java

@ -20,18 +20,17 @@ import com.fasterxml.jackson.databind.node.TextNode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.edge.TbEdgeService; import org.thingsboard.server.service.entitiy.edge.TbEdgeService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -46,7 +45,7 @@ public class EdgeBulkImportService extends AbstractBulkImportService<Edge> {
@Override @Override
protected void setEntityFields(Edge entity, Map<BulkImportColumnType, String> fields) { protected void setEntityFields(Edge entity, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity);
fields.forEach((columnType, value) -> { fields.forEach((columnType, value) -> {
switch (columnType) { switch (columnType) {
case NAME: case NAME:

6
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -42,9 +42,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -70,7 +70,7 @@ public class DefaultMailService implements MailService {
private final MessageSource messages; private final MessageSource messages;
private final Configuration freemarkerConfig; private final Configuration freemarkerConfig;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private static final long DEFAULT_TIMEOUT = 10_000; private static final long DEFAULT_TIMEOUT = 10_000;
@ -90,7 +90,7 @@ public class DefaultMailService implements MailService {
private long timeout; private long timeout;
public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageClient apiUsageClient) { public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) {
this.messages = messages; this.messages = messages;
this.freemarkerConfig = freemarkerConfig; this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService; this.adminSettingsService = adminSettingsService;

249
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -1,249 +0,0 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.util.Pair;
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.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
/**
* Created by ashvayka on 26.09.18.
*/
@Slf4j
@SuppressWarnings("UnstableApiUsage")
public abstract class AbstractJsInvokeService implements JsInvokeService {
private final TbApiUsageStateService apiUsageStateService;
private final TbApiUsageClient apiUsageClient;
protected ScheduledExecutorService timeoutExecutorService;
protected final Map<UUID, Pair<String, String>> scriptIdToNameAndHashMap = new ConcurrentHashMap<>();
protected final Map<UUID, DisableListInfo> disabledFunctions = 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(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
}
public void init(long maxRequestsTimeout) {
if (maxRequestsTimeout > 0) {
timeoutExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("nashorn-js-timeout"));
}
}
public void stop() {
if (timeoutExecutorService != null) {
timeoutExecutorService.shutdownNow();
}
}
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
if (scriptBodySizeExceeded(scriptBody)) {
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
}
UUID scriptId = UUID.randomUUID();
String scriptHash = hash(tenantId, scriptBody);
String functionName = constructFunctionName(scriptId, scriptHash);
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
return doEval(scriptId, scriptHash, functionName, jsScript);
} else {
return error("JS Execution is disabled due to API limits!");
}
}
protected String constructFunctionName(UUID scriptId, String scriptHash) {
return "invokeInternal_" + scriptId.toString().replace('-', '_');
}
@Override
public ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
Pair<String, String> nameAndHash = scriptIdToNameAndHashMap.get(scriptId);
if (nameAndHash == null) {
return error("No compiled script found for scriptId: [" + scriptId + "]!");
}
String functionName = nameAndHash.getFirst();
String scriptHash = nameAndHash.getSecond();
if (!isDisabled(scriptId)) {
if (argsSizeExceeded(args)) {
return scriptExecutionError(scriptId, format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize()));
}
apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return Futures.transformAsync(doInvokeFunction(scriptId, scriptHash, functionName, args), output -> {
String result = output.toString();
if (resultSizeExceeded(result)) {
return scriptExecutionError(scriptId, format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()));
}
return Futures.immediateFuture(result);
}, MoreExecutors.directExecutor());
} else {
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return error(message);
}
} else {
return error("JS Execution is disabled due to API limits!");
}
}
@Override
public ListenableFuture<Void> release(UUID scriptId) {
Pair<String, String> nameAndHash = scriptIdToNameAndHashMap.get(scriptId);
if (nameAndHash != null) {
try {
scriptIdToNameAndHashMap.remove(scriptId);
disabledFunctions.remove(scriptId);
doRelease(scriptId, nameAndHash.getSecond(), nameAndHash.getFirst());
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
return Futures.immediateFuture(null);
}
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String scriptBody);
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args);
protected abstract void doRelease(UUID scriptId, String scriptHash, String functionName) throws Exception;
protected abstract int getMaxErrors();
protected abstract long getMaxBlacklistDuration();
protected String hash(TenantId tenantId, String scriptBody) {
return Hashing.murmur3_128().newHasher()
.putLong(tenantId.getId().getMostSignificantBits())
.putLong(tenantId.getId().getLeastSignificantBits())
.putUnencodedChars(scriptBody)
.hash().toString();
}
protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo());
log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
disableListInfo.get(), scriptId, t, t.getCause(), scriptBody);
disableListInfo.incrementAndGet();
}
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();
}
}
return totalArgsSize > getMaxTotalArgsSize();
}
private boolean resultSizeExceeded(String result) {
if (getMaxResultSize() <= 0) return false;
return result.length() > getMaxResultSize();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
if (scriptType == JsScriptType.RULE_NODE_SCRIPT) {
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
}
throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
}
private boolean isDisabled(UUID scriptId) {
DisableListInfo errorCount = disabledFunctions.get(scriptId);
if (errorCount != null) {
if (errorCount.getExpirationTime() <= System.currentTimeMillis()) {
disabledFunctions.remove(scriptId);
return false;
} else {
return errorCount.get() >= getMaxErrors();
}
} else {
return false;
}
}
private <T> ListenableFuture<T> error(String message) {
return Futures.immediateFailedFuture(new RuntimeException(message));
}
private <T> ListenableFuture<T> scriptExecutionError(UUID scriptId, String errorMsg) {
RuntimeException error = new RuntimeException(errorMsg);
onScriptExecutionError(scriptId, error, null);
return Futures.immediateFailedFuture(error);
}
private class DisableListInfo {
private final AtomicInteger counter;
private long expirationTime;
private DisableListInfo() {
this.counter = new AtomicInteger(0);
}
public int get() {
return counter.get();
}
public int incrementAndGet() {
int result = counter.incrementAndGet();
expirationTime = System.currentTimeMillis() + getMaxBlacklistDuration();
return result;
}
public long getExpirationTime() {
return expirationTime;
}
}
}

185
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java

@ -1,185 +0,0 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
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.data.util.Pair;
import org.springframework.scheduling.annotation.Scheduled;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
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.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeService {
private NashornSandbox sandbox;
private ScriptEngine engine;
private ExecutorService monitorExecutorService;
private final AtomicInteger jsPushedMsgs = new AtomicInteger(0);
private final AtomicInteger jsInvokeMsgs = new AtomicInteger(0);
private final AtomicInteger jsEvalMsgs = new AtomicInteger(0);
private final AtomicInteger jsFailedMsgs = new AtomicInteger(0);
private final AtomicInteger jsTimeoutMsgs = new AtomicInteger(0);
private final FutureCallback<UUID> evalCallback = new JsStatCallback<>(jsEvalMsgs, jsTimeoutMsgs, jsFailedMsgs);
private final FutureCallback<Object> invokeCallback = new JsStatCallback<>(jsInvokeMsgs, jsTimeoutMsgs, jsFailedMsgs);
private final ReentrantLock evalLock = new ReentrantLock();
@Getter
private final JsExecutorService jsExecutor;
@Value("${js.local.max_requests_timeout:0}")
private long maxRequestsTimeout;
@Value("${js.local.stats.enabled:false}")
private boolean statsEnabled;
public AbstractNashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) {
super(apiUsageStateService, apiUsageClient);
this.jsExecutor = jsExecutor;
}
@Scheduled(fixedDelayString = "${js.local.stats.print_interval_ms:10000}")
public void printStats() {
if (statsEnabled) {
int pushedMsgs = jsPushedMsgs.getAndSet(0);
int invokeMsgs = jsInvokeMsgs.getAndSet(0);
int evalMsgs = jsEvalMsgs.getAndSet(0);
int failed = jsFailedMsgs.getAndSet(0);
int timedOut = jsTimeoutMsgs.getAndSet(0);
if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) {
log.info("Nashorn JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut);
}
}
}
@PostConstruct
public void init() {
super.init(maxRequestsTimeout);
if (useJsSandbox()) {
sandbox = NashornSandboxes.create();
monitorExecutorService = ThingsBoardExecutors.newWorkStealingPool(getMonitorThreadPoolSize(), "nashorn-js-monitor");
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(getMaxCpuTime());
sandbox.allowNoBraces(false);
sandbox.allowLoadFunctions(true);
sandbox.setMaxPreparedStatements(30);
} else {
ScriptEngineManager factory = new ScriptEngineManager();
engine = factory.getEngineByName("nashorn");
}
}
@PreDestroy
public void stop() {
super.stop();
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
}
protected abstract boolean useJsSandbox();
protected abstract int getMonitorThreadPoolSize();
protected abstract long getMaxCpuTime();
@Override
protected ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String jsScript) {
jsPushedMsgs.incrementAndGet();
ListenableFuture<UUID> result = jsExecutor.executeAsync(() -> {
try {
evalLock.lock();
try {
if (useJsSandbox()) {
sandbox.eval(jsScript);
} else {
engine.eval(jsScript);
}
} finally {
evalLock.unlock();
}
scriptIdToNameAndHashMap.put(scriptId, Pair.of(functionName, scriptHash));
return scriptId;
} catch (Exception e) {
log.debug("Failed to compile JS script: {}", e.getMessage(), e);
throw new ExecutionException(e);
}
});
if (maxRequestsTimeout > 0) {
result = Futures.withTimeout(result, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
Futures.addCallback(result, evalCallback, MoreExecutors.directExecutor());
return result;
}
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args) {
jsPushedMsgs.incrementAndGet();
ListenableFuture<Object> result = jsExecutor.executeAsync(() -> {
try {
if (useJsSandbox()) {
return sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
} else {
return ((Invocable) engine).invokeFunction(functionName, args);
}
} catch (ScriptException e) {
throw new ExecutionException(e);
} catch (Exception e) {
onScriptExecutionError(scriptId, e, functionName);
throw new ExecutionException(e);
}
});
if (maxRequestsTimeout > 0) {
result = Futures.withTimeout(result, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
Futures.addCallback(result, invokeCallback, MoreExecutors.directExecutor());
return result;
}
protected void doRelease(UUID scriptId, String scriptHash, String functionName) throws ScriptException {
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;");
} else {
engine.eval(functionName + " = undefined;");
}
}
}

64
application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java

@ -1,64 +0,0 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.concurrent.TimeUnit;
@Slf4j
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "local", matchIfMissing = true)
@Service
public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
@Value("${js.local.use_js_sandbox}")
private boolean useJsSandbox;
@Getter
@Value("${js.local.monitor_thread_pool_size}")
private int monitorThreadPoolSize;
@Getter
@Value("${js.local.max_cpu_time}")
private long maxCpuTime;
@Getter
@Value("${js.local.max_errors}")
private int maxErrors;
@Value("${js.local.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec;
public NashornJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient, JsExecutorService jsExecutor) {
super(apiUsageStateService, apiUsageClient, jsExecutor);
}
@Override
protected boolean useJsSandbox() {
return useJsSandbox;
}
@Override
protected long getMaxBlacklistDuration() {
return TimeUnit.SECONDS.toMillis(maxBlackListDurationSec);
}
}

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

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

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

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

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

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

5
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java

@ -25,6 +25,8 @@ import javax.servlet.http.HttpServletResponse;
@Component @Component
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> { public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
public static final String PREV_URI_PARAMETER = "prevUri";
public static final String PREV_URI_COOKIE_NAME = "prev_uri";
private static final int cookieExpireSeconds = 180; private static final int cookieExpireSeconds = 180;
@Override @Override
@ -40,6 +42,9 @@ public class HttpCookieOAuth2AuthorizationRequestRepository implements Authoriza
CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
return; return;
} }
if (request.getParameter(PREV_URI_PARAMETER) != null) {
CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PARAMETER), cookieExpireSeconds);
}
CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds); CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds);
} }

9
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -37,13 +37,17 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository.PREV_URI_COOKIE_NAME;
@Slf4j @Slf4j
@Component(value = "oauth2AuthenticationSuccessHandler") @Component(value = "oauth2AuthenticationSuccessHandler")
@TbCoreComponent @TbCoreComponent
@ -85,6 +89,11 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
baseUrl = callbackUrlScheme + ":"; baseUrl = callbackUrlScheme + ":";
} else { } else {
baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
Optional<Cookie> prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME);
if (prevUrlOpt.isPresent()) {
baseUrl += prevUrlOpt.get().getValue();
CookieUtils.deleteCookie(request, response, PREV_URI_COOKIE_NAME);
}
} }
try { try {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;

6
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -30,9 +30,9 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@ -45,11 +45,11 @@ public class DefaultSmsService implements SmsService {
private final SmsSenderFactory smsSenderFactory; private final SmsSenderFactory smsSenderFactory;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private SmsSender smsSender; private SmsSender smsSender;
public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) { public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageReportClient apiUsageClient) {
this.smsSenderFactory = smsSenderFactory; this.smsSenderFactory = smsSenderFactory;
this.adminSettingsService = adminSettingsService; this.adminSettingsService = adminSettingsService;
this.apiUsageStateService = apiUsageStateService; this.apiUsageStateService = apiUsageStateService;

7
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.sync.ie.importing.csv; package org.thingsboard.server.service.sync.ie.importing.csv;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
@ -25,8 +26,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
@ -165,6 +168,10 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
protected abstract EntityType getEntityType(); protected abstract EntityType getEntityType();
protected ObjectNode getOrCreateAdditionalInfoObj(HasAdditionalInfo entity) {
return entity.getAdditionalInfo() == null || entity.getAdditionalInfo().isNull() ?
JacksonUtil.newObjectNode() : (ObjectNode) entity.getAdditionalInfo();
}
private void saveKvs(SecurityUser user, E entity, Map<BulkImportRequest.ColumnMapping, ParsedValue> data) { private void saveKvs(SecurityUser user, E entity, Map<BulkImportRequest.ColumnMapping, ParsedValue> data) {
Arrays.stream(BulkImportColumnType.values()) Arrays.stream(BulkImportColumnType.values())

4
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -537,7 +537,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId()); var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
@ -564,7 +564,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
} }
var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId()); var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());

6
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -40,11 +40,11 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.SubscriptionManagerService;
@ -61,13 +61,13 @@ import java.util.Optional;
public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService { public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService {
private final AlarmService alarmService; private final AlarmService alarmService;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
public DefaultAlarmSubscriptionService(TbClusterService clusterService, public DefaultAlarmSubscriptionService(TbClusterService clusterService,
PartitionService partitionService, PartitionService partitionService,
AlarmService alarmService, AlarmService alarmService,
TbApiUsageClient apiUsageClient, TbApiUsageReportClient apiUsageClient,
TbApiUsageStateService apiUsageStateService) { TbApiUsageStateService apiUsageStateService) {
super(clusterService, partitionService); super(clusterService, partitionService);
this.alarmService = alarmService; this.alarmService = alarmService;

6
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -43,11 +43,11 @@ import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService; import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -77,7 +77,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
private final AttributesService attrService; private final AttributesService attrService;
private final TimeseriesService tsService; private final TimeseriesService tsService;
private final TbEntityViewService tbEntityViewService; private final TbEntityViewService tbEntityViewService;
private final TbApiUsageClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService; private final TbApiUsageStateService apiUsageStateService;
private ExecutorService tsCallBackExecutor; private ExecutorService tsCallBackExecutor;
@ -87,7 +87,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Lazy TbEntityViewService tbEntityViewService, @Lazy TbEntityViewService tbEntityViewService,
TbClusterService clusterService, TbClusterService clusterService,
PartitionService partitionService, PartitionService partitionService,
TbApiUsageClient apiUsageClient, TbApiUsageReportClient apiUsageClient,
TbApiUsageStateService apiUsageStateService) { TbApiUsageStateService apiUsageStateService) {
super(clusterService, partitionService); super(clusterService, partitionService);
this.attrService = attrService; this.attrService = attrService;

27
application/src/main/resources/thingsboard.yml

@ -268,7 +268,7 @@ sql:
audit_logs: audit_logs:
partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week
# Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks
batch_sort: "${SQL_BATCH_SORT:false}" batch_sort: "${SQL_BATCH_SORT:true}"
# Specify whether to remove null characters from strValue of attributes and timeseries before insert # Specify whether to remove null characters from strValue of attributes and timeseries before insert
remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}" remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
# Specify whether to log database queries and their parameters generated by entity query repository # Specify whether to log database queries and their parameters generated by entity query repository
@ -332,8 +332,6 @@ actors:
rule: rule:
# Specify thread pool size for database request callbacks executor service # Specify thread pool size for database request callbacks executor service
db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:50}" db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:50}"
# Specify thread pool size for javascript executor service
js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
# Specify thread pool size for mail sender executor service # Specify thread pool size for mail sender executor service
mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:40}" mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:40}"
# Specify thread pool size for password reset emails # Specify thread pool size for password reset emails
@ -599,6 +597,25 @@ state:
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}" defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}" persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
mvel:
enabled: "${MVEL_ENABLED:true}"
max_total_args_size: "${MVEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${MVEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${MVEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed MVEL script execution memory
max_memory_limit_mb: "${MVEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed MVEL script execution errors before it will be blacklisted
max_errors: "${MVEL_MAX_ERRORS:3}"
# MVEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${MVEL_MAX_REQUEST_TIMEOUT:500}"
# Maximum time in seconds for black listed function to stay in the list.
max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}"
# Specify thread pool size for javascript executor service
thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}"
stats:
enabled: "${TB_MVEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}"
js: js:
evaluator: "${JS_EVALUATOR:local}" # local/remote evaluator: "${JS_EVALUATOR:local}" # local/remote
max_total_args_size: "${JS_MAX_TOTAL_ARGS_SIZE:100000}" max_total_args_size: "${JS_MAX_TOTAL_ARGS_SIZE:100000}"
@ -606,6 +623,8 @@ js:
max_script_body_size: "${JS_MAX_SCRIPT_BODY_SIZE:50000}" max_script_body_size: "${JS_MAX_SCRIPT_BODY_SIZE:50000}"
# Built-in JVM JavaScript environment properties # Built-in JVM JavaScript environment properties
local: local:
# Specify thread pool size for javascript executor service
js_thread_pool_size: "${LOCAL_JS_THREAD_POOL_SIZE:50}"
# Use Sandboxed (secured) JVM JavaScript environment # Use Sandboxed (secured) JVM JavaScript environment
use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}" use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}"
# Specify thread pool size for JavaScript sandbox resource monitor # Specify thread pool size for JavaScript sandbox resource monitor
@ -623,6 +642,8 @@ js:
print_interval_ms: "${TB_JS_LOCAL_STATS_PRINT_INTERVAL_MS:10000}" print_interval_ms: "${TB_JS_LOCAL_STATS_PRINT_INTERVAL_MS:10000}"
# Remote JavaScript environment properties # Remote JavaScript environment properties
remote: remote:
# Specify thread pool size for javascript executor service
js_thread_pool_size: "${REMOTE_JS_THREAD_POOL_SIZE:50}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}" max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}"
# Maximum time in seconds for black listed function to stay in the list. # Maximum time in seconds for black listed function to stay in the list.

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

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

128
application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"mvel.max_script_body_size=100",
"mvel.max_total_args_size=50",
"mvel.max_result_size=50",
"mvel.max_errors=2",
})
class MvelInvokeServiceTest extends AbstractControllerTest {
@Autowired
private MvelInvokeService invokeService;
@Value("${mvel.max_errors}")
private int maxJsErrors;
@Test
void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
int iterations = 100000;
UUID scriptId = evalScript("return msg.temperature > 20");
// warmup
ObjectNode msg = JacksonUtil.newObjectNode();
for (int i = 0; i < 100; i++) {
msg.put("temperature", i);
boolean expected = i > 20;
boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result);
}
long startTs = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
msg.put("temperature", i);
boolean expected = i > 20;
boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result);
}
long duration = System.currentTimeMillis() - startTs;
System.out.println(iterations + " invocations took: " + duration + "ms");
Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(1));
}
@Test
void givenTooBigScriptForEval_thenReturnError() {
String hugeScript = "var a = 'qwertyqwertywertyqwabababerqwertyqwertywertyqwabababerqwertyqwertywertyqwabababerqwertyqwertywertyqwabababerqwertyqwertywertyqwabababer'; return {a: a};";
assertThatThrownBy(() -> {
evalScript(hugeScript);
}).hasMessageContaining("body exceeds maximum allowed size");
}
@Test
void givenTooBigScriptInputArgs_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "return { msg: msg };";
String hugeMsg = "{\"input\":\"123456781234349\"}";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, hugeMsg);
}).hasMessageContaining("input arguments exceed maximum");
}
assertThatScriptIsBlocked(scriptId);
}
@Test
void whenScriptInvocationResultIsTooBig_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "var s = 'a'; for(int i=0; i<50; i++){ s +='a';} return { s: s};";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("result exceeds maximum allowed size");
}
assertThatScriptIsBlocked(scriptId);
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("invocation is blocked due to maximum error");
}
private UUID evalScript(String script) throws ExecutionException, InterruptedException {
return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script, "msg", "metadata", "msgType").get();
}
private String invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException {
var msg = JacksonUtil.fromString(str, Map.class);
return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString();
}
}

38
application/src/test/java/org/thingsboard/server/service/script/LocalJsInvokeServiceTest.java → application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java

@ -15,16 +15,22 @@
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.js.NashornJsInvokeService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -35,14 +41,38 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
"js.max_result_size=50", "js.max_result_size=50",
"js.local.max_errors=2" "js.local.max_errors=2"
}) })
class LocalJsInvokeServiceTest extends AbstractControllerTest { class NashornJsInvokeServiceTest extends AbstractControllerTest {
@Autowired @Autowired
private NashornJsInvokeService jsInvokeService; private NashornJsInvokeService invokeService;
@Value("${js.local.max_errors}") @Value("${js.local.max_errors}")
private int maxJsErrors; private int maxJsErrors;
@Test
void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
int iterations = 1000;
UUID scriptId = evalScript("return msg.temperature > 20");
// warmup
ObjectNode msg = JacksonUtil.newObjectNode();
for (int i = 0; i < 100; i++) {
msg.put("temperature", i);
boolean expected = i > 20;
boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result);
}
long startTs = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
msg.put("temperature", i);
boolean expected = i > 20;
boolean result = Boolean.valueOf(invokeScript(scriptId, JacksonUtil.toString(msg)));
Assert.assertEquals(expected, result);
}
long duration = System.currentTimeMillis() - startTs;
System.out.println(iterations + " invocations took: " + duration + "ms");
Assert.assertTrue(duration < TimeUnit.MINUTES.toMillis(1));
}
@Test @Test
void givenTooBigScriptForEval_thenReturnError() { void givenTooBigScriptForEval_thenReturnError() {
String hugeScript = "var a = 'qwertyqwertywertyqwabababer'; return {a: a};"; String hugeScript = "var a = 'qwertyqwertywertyqwabababer'; return {a: a};";
@ -86,11 +116,11 @@ class LocalJsInvokeServiceTest extends AbstractControllerTest {
} }
private UUID evalScript(String script) throws ExecutionException, InterruptedException { private UUID evalScript(String script) throws ExecutionException, InterruptedException {
return jsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, script).get(); return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script).get();
} }
private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException { private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException {
return jsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get(); return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString();
} }
} }

34
application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java

@ -21,19 +21,21 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest;
import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse; import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse;
import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -57,13 +59,13 @@ class RemoteJsInvokeServiceTest {
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
TbApiUsageStateService apiUsageStateService = mock(TbApiUsageStateService.class); TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class);
ApiUsageState apiUsageState = mock(ApiUsageState.class); ApiUsageState apiUsageState = mock(ApiUsageState.class);
when(apiUsageState.isJsExecEnabled()).thenReturn(true); when(apiUsageState.isJsExecEnabled()).thenReturn(true);
when(apiUsageStateService.getApiUsageState(any())).thenReturn(apiUsageState); when(apiUsageStateClient.getApiUsageState(any())).thenReturn(apiUsageState);
TbApiUsageClient apiUsageClient = mock(TbApiUsageClient.class); TbApiUsageReportClient apiUsageReportClient = mock(TbApiUsageReportClient.class);
remoteJsInvokeService = new RemoteJsInvokeService(apiUsageStateService, apiUsageClient); remoteJsInvokeService = new RemoteJsInvokeService(Optional.of(apiUsageStateClient), Optional.of(apiUsageReportClient));
jsRequestTemplate = mock(TbQueueRequestTemplate.class); jsRequestTemplate = mock(TbQueueRequestTemplate.class);
remoteJsInvokeService.requestTemplate = jsRequestTemplate; remoteJsInvokeService.requestTemplate = jsRequestTemplate;
} }
@ -77,7 +79,7 @@ class RemoteJsInvokeServiceTest {
public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
mockJsEvalResponse(); mockJsEvalResponse();
String scriptBody = "return { a: 'b'};"; String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get(); UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
reset(jsRequestTemplate); reset(jsRequestTemplate);
String expectedInvocationResult = "scriptInvocationResult"; String expectedInvocationResult = "scriptInvocationResult";
@ -90,7 +92,7 @@ class RemoteJsInvokeServiceTest {
.when(jsRequestTemplate).send(any()); .when(jsRequestTemplate).send(any());
ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class); ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class);
Object invocationResult = remoteJsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get(); Object invocationResult = remoteJsInvokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get();
verify(jsRequestTemplate).send(jsRequestCaptor.capture()); verify(jsRequestTemplate).send(jsRequestCaptor.capture());
JsInvokeProtos.JsInvokeRequest jsInvokeRequestMade = jsRequestCaptor.getValue().getValue().getInvokeRequest(); JsInvokeProtos.JsInvokeRequest jsInvokeRequestMade = jsRequestCaptor.getValue().getValue().getInvokeRequest();
@ -103,7 +105,7 @@ class RemoteJsInvokeServiceTest {
public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
mockJsEvalResponse(); mockJsEvalResponse();
String scriptBody = "return { a: 'b'};"; String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get(); UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
reset(jsRequestTemplate); reset(jsRequestTemplate);
doReturn(Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder() doReturn(Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
@ -128,7 +130,7 @@ class RemoteJsInvokeServiceTest {
})); }));
ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestsCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class); ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestsCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class);
Object invocationResult = remoteJsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get(); Object invocationResult = remoteJsInvokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get();
verify(jsRequestTemplate, times(2)).send(jsRequestsCaptor.capture()); verify(jsRequestTemplate, times(2)).send(jsRequestsCaptor.capture());
List<TbProtoJsQueueMsg<RemoteJsRequest>> jsInvokeRequestsMade = jsRequestsCaptor.getAllValues(); List<TbProtoJsQueueMsg<RemoteJsRequest>> jsInvokeRequestsMade = jsRequestsCaptor.getAllValues();
@ -159,19 +161,19 @@ class RemoteJsInvokeServiceTest {
Set<String> scriptHashes = new HashSet<>(); Set<String> scriptHashes = new HashSet<>();
String tenant1Script1Hash = null; String tenant1Script1Hash = null;
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
UUID scriptUuid = remoteJsInvokeService.eval(tenantId1, JsScriptType.RULE_NODE_SCRIPT, scriptBody1).get(); UUID scriptUuid = remoteJsInvokeService.eval(tenantId1, ScriptType.RULE_NODE_SCRIPT, scriptBody1).get();
tenant1Script1Hash = getScriptHash(scriptUuid); tenant1Script1Hash = getScriptHash(scriptUuid);
scriptHashes.add(tenant1Script1Hash); scriptHashes.add(tenant1Script1Hash);
} }
assertThat(scriptHashes).as("Unique scripts ids").size().isOne(); assertThat(scriptHashes).as("Unique scripts ids").size().isOne();
TenantId tenantId2 = TenantId.fromUUID(UUID.randomUUID()); TenantId tenantId2 = TenantId.fromUUID(UUID.randomUUID());
UUID scriptUuid = remoteJsInvokeService.eval(tenantId2, JsScriptType.RULE_NODE_SCRIPT, scriptBody1).get(); UUID scriptUuid = remoteJsInvokeService.eval(tenantId2, ScriptType.RULE_NODE_SCRIPT, scriptBody1).get();
String tenant2Script1Id = getScriptHash(scriptUuid); String tenant2Script1Id = getScriptHash(scriptUuid);
assertThat(tenant2Script1Id).isNotEqualTo(tenant1Script1Hash); assertThat(tenant2Script1Id).isNotEqualTo(tenant1Script1Hash);
String scriptBody2 = scriptBody1 + ";;"; String scriptBody2 = scriptBody1 + ";;";
scriptUuid = remoteJsInvokeService.eval(tenantId2, JsScriptType.RULE_NODE_SCRIPT, scriptBody2).get(); scriptUuid = remoteJsInvokeService.eval(tenantId2, ScriptType.RULE_NODE_SCRIPT, scriptBody2).get();
String tenant2Script2Id = getScriptHash(scriptUuid); String tenant2Script2Id = getScriptHash(scriptUuid);
assertThat(tenant2Script2Id).isNotEqualTo(tenant2Script1Id); assertThat(tenant2Script2Id).isNotEqualTo(tenant2Script1Id);
} }
@ -180,8 +182,8 @@ class RemoteJsInvokeServiceTest {
public void whenReleasingScript_thenCheckForHashUsages() throws Exception { public void whenReleasingScript_thenCheckForHashUsages() throws Exception {
mockJsEvalResponse(); mockJsEvalResponse();
String scriptBody = "return { a: 'b'};"; String scriptBody = "return { a: 'b'};";
UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get(); UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
UUID scriptId2 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get(); UUID scriptId2 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
String scriptHash = getScriptHash(scriptId1); String scriptHash = getScriptHash(scriptId1);
assertThat(scriptHash).isEqualTo(getScriptHash(scriptId2)); assertThat(scriptHash).isEqualTo(getScriptHash(scriptId2));
reset(jsRequestTemplate); reset(jsRequestTemplate);
@ -203,7 +205,7 @@ class RemoteJsInvokeServiceTest {
} }
private String getScriptHash(UUID scriptUuid) { private String getScriptHash(UUID scriptUuid) {
return remoteJsInvokeService.scriptIdToNameAndHashMap.get(scriptUuid).getSecond(); return remoteJsInvokeService.getScriptHash(scriptUuid);
} }
private void mockJsEvalResponse() { private void mockJsEvalResponse() {

196
application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java

@ -0,0 +1,196 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.system;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Uninterruptibles;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.controller.AbstractControllerTest;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Illia Barkov
*/
@Slf4j
public abstract class BaseRestApiLimitsTest extends AbstractControllerTest {
private static int MESSAGES_LIMIT = 10;
private static int TIME_FOR_LIMIT = 5;
TenantProfile tenantProfile;
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
@Before
public void before() throws Exception {
loginSysAdmin();
tenantProfile = getDefaultTenantProfile();
logout();
}
@After
public void after() throws Exception {
logout();
loginSysAdmin();
saveTenantProfileWitConfiguration(tenantProfile, new DefaultTenantProfileConfiguration());
logout();
service.shutdown();
}
@Test
public void testCustomerRestApiLimits() throws Exception {
loginSysAdmin();
String customerRestLimit = MESSAGES_LIMIT + ":" + TIME_FOR_LIMIT;
DefaultTenantProfileConfiguration configurationWithCustomerRestLimits = createTenantProfileConfigurationWithRestLimits(null, customerRestLimit);
saveTenantProfileWitConfiguration(tenantProfile, configurationWithCustomerRestLimits);
logout();
loginCustomerUser();
for (int i = 0; i < MESSAGES_LIMIT; i++) {
doGet("/api/device/types").andExpect(status().isOk());
}
doGet("/api/device/types").andExpect(status().is4xxClientError());
}
@Test
public void testTenantRestApiLimits() throws Exception {
loginSysAdmin();
String tenantRestLimit = MESSAGES_LIMIT + ":" + TIME_FOR_LIMIT;
DefaultTenantProfileConfiguration configurationWithTenantRestLimits = createTenantProfileConfigurationWithRestLimits(tenantRestLimit, null);
saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits);
logout();
loginCustomerUser();
for (int i = 0; i < MESSAGES_LIMIT; i++) {
doGet("/api/device/types").andExpect(status().isOk());
}
doGet("/api/device/types").andExpect(status().is4xxClientError());
}
@Test
public void testCustomerRestApiLimitsWithAsyncMethod() throws Exception {
loginSysAdmin();
String tenantRestLimit = MESSAGES_LIMIT + ":" + TIME_FOR_LIMIT;
DefaultTenantProfileConfiguration configurationWithTenantRestLimits = createTenantProfileConfigurationWithRestLimits(tenantRestLimit, null);
saveTenantProfileWitConfiguration(tenantProfile, configurationWithTenantRestLimits);
logout();
loginTenantAdmin();
List<ListenableFuture<ResultActions>> attributesRequests = new ArrayList<>();
doGet("/api/plugins/telemetry/" + tenantId.getEntityType() + "/" + tenantId.getId().toString() + "/values/attributes").andExpect(status().isOk());
Thread.sleep(TimeUnit.SECONDS.toMillis(TIME_FOR_LIMIT)); // Wait to initialization for bucket4j
for (int i = 0; i < MESSAGES_LIMIT; i++) {
attributesRequests.add(service.submit(() -> doGet("/api/plugins/telemetry/" + tenantId.getEntityType() + "/" + tenantId.getId().toString() + "/values/attributes")));
}
List<ResultActions> lists = blockForResponses(attributesRequests);
for (ResultActions resultActions : lists) {
resultActions.andExpect(status().isOk());
}
doGet("/api/plugins/telemetry/" + tenantId.getEntityType() + "/" + tenantId.getId().toString() + "/values/attributes").andExpect(status().is4xxClientError());
}
private TenantProfile getDefaultTenantProfile() throws Exception {
PageLink pageLink = new PageLink(17);
PageData<TenantProfile> pageData = doGetTypedWithPageLink("/api/tenantProfiles?",
new TypeReference<>(){}, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
List<TenantProfile> tenantProfiles = new ArrayList<>(pageData.getData());
Optional<TenantProfile> optionalDefaultProfile = tenantProfiles.stream().filter(TenantProfile::isDefault).reduce((a, b) -> null);
Assert.assertTrue(optionalDefaultProfile.isPresent());
return optionalDefaultProfile.get();
}
List<ResultActions> blockForResponses(List<ListenableFuture<ResultActions>> futures) throws ExecutionException {
ListenableFuture<List<ResultActions>> futureOfList = Futures.allAsList(futures);
List<ResultActions> responses;
try {
responses = futureOfList.get(20, TimeUnit.SECONDS);
} catch (TimeoutException | InterruptedException | ExecutionException e) {
responses = new ArrayList<>();
for (ListenableFuture<ResultActions> future : futures) {
if (future.isDone()) {
responses.add(Uninterruptibles.getUninterruptibly(future));
}
}
}
return responses;
}
private DefaultTenantProfileConfiguration createTenantProfileConfigurationWithRestLimits(String tenantLimits, String customerLimits) {
DefaultTenantProfileConfiguration.DefaultTenantProfileConfigurationBuilder builder = DefaultTenantProfileConfiguration.builder();
builder.tenantServerRestLimitsConfiguration(tenantLimits);
builder.customerServerRestLimitsConfiguration(customerLimits);
return builder.build();
}
private void saveTenantProfileWitConfiguration(TenantProfile tenantProfile, TenantProfileConfiguration tenantProfileConfiguration) {
TenantProfileData tenantProfileData = tenantProfile.getProfileData();
tenantProfileData.setConfiguration(tenantProfileConfiguration);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
}
}

24
application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java

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

2
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -33,6 +33,8 @@ public interface UserService {
User findUserByEmail(TenantId tenantId, String email); User findUserByEmail(TenantId tenantId, String email);
User findUserByTenantIdAndEmail(TenantId tenantId, String email);
User saveUser(User user); User saveUser(User user);
UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId);

4
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -45,7 +45,7 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn
@ToString(exclude = {"image", "profileDataBytes"}) @ToString(exclude = {"image", "profileDataBytes"})
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, ExportableEntity<DeviceProfileId> { public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity<DeviceProfileId> {
private static final long serialVersionUID = 6998485460273302018L; private static final long serialVersionUID = 6998485460273302018L;
@ -141,7 +141,7 @@ public class DeviceProfile extends SearchTextBased<DeviceProfileId> implements H
} }
@ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the device profile is not specified during device creation.") @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the device profile is not specified during device creation.")
public boolean isDefault(){ public boolean isDefault() {
return isDefault; return isDefault;
} }

15
application/src/main/java/org/thingsboard/server/service/script/JsInvokeRequest.java → common/data/src/main/java/org/thingsboard/server/common/data/HasRuleEngineProfile.java

@ -13,17 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.server.common.data;
import java.util.List; import org.thingsboard.server.common.data.id.RuleChainId;
/** public interface HasRuleEngineProfile {
* Created by ashvayka on 25.09.18.
*/ RuleChainId getDefaultRuleChainId();
public class JsInvokeRequest {
private String scriptId; String getDefaultQueueName();
private String scriptBody;
private List<String> args;
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -146,6 +146,16 @@ public class StringUtils {
return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep);
} }
public static boolean containedByAny(String searchString, String... strings) {
if (searchString == null) return false;
for (String string : strings) {
if (string != null && string.contains(searchString)) {
return true;
}
}
return false;
}
public static String randomNumeric(int length) { public static String randomNumeric(int length) {
return RandomStringUtils.randomNumeric(length); return RandomStringUtils.randomNumeric(length);
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java

@ -23,6 +23,7 @@ import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
@ -37,7 +38,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ToString(exclude = {"image"}) @ToString(exclude = {"image"})
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class AssetProfile extends SearchTextBased<AssetProfileId> implements HasName, HasTenantId, ExportableEntity<AssetProfileId> { public class AssetProfile extends SearchTextBased<AssetProfileId> implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity<AssetProfileId> {
private static final long serialVersionUID = 6998485460273302018L; private static final long serialVersionUID = 6998485460273302018L;

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

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

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettings.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.common.data.sync.vc; package org.thingsboard.server.common.data.sync.vc;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@ -32,6 +31,7 @@ public class RepositorySettings implements Serializable {
private String privateKey; private String privateKey;
private String privateKeyPassword; private String privateKeyPassword;
private String defaultBranch; private String defaultBranch;
private boolean readOnly;
public RepositorySettings() { public RepositorySettings() {
} }
@ -45,5 +45,6 @@ public class RepositorySettings implements Serializable {
this.privateKey = settings.getPrivateKey(); this.privateKey = settings.getPrivateKey();
this.privateKeyPassword = settings.getPrivateKeyPassword(); this.privateKeyPassword = settings.getPrivateKeyPassword();
this.defaultBranch = settings.getDefaultBranch(); this.defaultBranch = settings.getDefaultBranch();
this.readOnly = settings.isReadOnly();
} }
} }

30
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/RepositorySettingsInfo.java

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

7
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java

@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.widget; package org.thingsboard.server.common.data.widget;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -31,7 +33,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel @ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements HasTenantId, ExportableEntity<WidgetsBundleId> { public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements HasName, HasTenantId, ExportableEntity<WidgetsBundleId> {
private static final long serialVersionUID = -7627368878362410489L; private static final long serialVersionUID = -7627368878362410489L;
@ -109,8 +111,9 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
return getTitle(); return getTitle();
} }
@JsonIgnore @ApiModelProperty(position = 3, value = "Same as title of the Widget Bundle. Read-only field. Update the 'title' to change the 'name' of the Widget Bundle.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Override @Override
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() { public String getName() {
return title; return title;
} }

1
common/pom.xml

@ -47,6 +47,7 @@
<module>coap-server</module> <module>coap-server</module>
<module>edge-api</module> <module>edge-api</module>
<module>version-control</module> <module>version-control</module>
<module>script</module>
</modules> </modules>
</project> </project>

7
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java → common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java

@ -17,9 +17,7 @@ package org.thingsboard.server.queue.usagestats;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -28,6 +26,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
@ -48,7 +47,7 @@ import java.util.concurrent.atomic.AtomicLong;
@Component @Component
@Slf4j @Slf4j
public class DefaultTbApiUsageClient implements TbApiUsageClient { public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
@Value("${usage.stats.report.enabled:true}") @Value("${usage.stats.report.enabled:true}")
private boolean enabled; private boolean enabled;
@ -64,7 +63,7 @@ public class DefaultTbApiUsageClient implements TbApiUsageClient {
private final TbQueueProducerProvider producerProvider; private final TbQueueProducerProvider producerProvider;
private TbQueueProducer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgProducer; private TbQueueProducer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> msgProducer;
public DefaultTbApiUsageClient(PartitionService partitionService, SchedulerComponent scheduler, TbQueueProducerProvider producerProvider) { public DefaultTbApiUsageReportClient(PartitionService partitionService, SchedulerComponent scheduler, TbQueueProducerProvider producerProvider) {
this.partitionService = partitionService; this.partitionService = partitionService;
this.scheduler = scheduler; this.scheduler = scheduler;
this.producerProvider = producerProvider; this.producerProvider = producerProvider;

42
common/script/pom.xml

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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</groupId>
<version>3.4.2-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
<artifactId>script</artifactId>
<packaging>pom</packaging>
<name>Thingsboard Script Invoke Commons</name>
<url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
</properties>
<modules>
<module>script-api</module>
<module>remote-js-client</module>
</modules>
</project>

89
common/script/remote-js-client/pom.xml

@ -0,0 +1,89 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>3.4.2-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>
<artifactId>remote-js-client</artifactId>
<packaging>jar</packaging>
<name>Thingsboard Server JS Client for remote JS execution</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>queue</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common.script</groupId>
<artifactId>script-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.rule-engine</groupId>
<artifactId>rule-engine-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>message</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>

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

@ -22,7 +22,7 @@ import org.thingsboard.common.util.AbstractListeningExecutor;
@Component @Component
public class JsExecutorService extends AbstractListeningExecutor { public class JsExecutorService extends AbstractListeningExecutor {
@Value("${actors.rule.js_thread_pool_size}") @Value("${js.remote.js_thread_pool_size:50}")
private int jsExecutorThreadPoolSize; private int jsExecutorThreadPoolSize;
@Override @Override

218
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java → common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -15,39 +15,40 @@
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.script.api.js.AbstractJsInvokeService;
import org.thingsboard.script.api.js.JsScriptInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.gen.js.JsInvokeProtos; import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.js.JsInvokeProtos.JsInvokeErrorCode;
import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoJsQueueMsg; import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -56,11 +57,13 @@ import java.util.concurrent.locks.ReentrantLock;
@Service @Service
public class RemoteJsInvokeService extends AbstractJsInvokeService { public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Getter
@Value("${queue.js.max_eval_requests_timeout}") @Value("${queue.js.max_eval_requests_timeout}")
private long maxEvalRequestsTimeout; private long maxEvalRequestsTimeout;
@Getter
@Value("${queue.js.max_requests_timeout}") @Value("${queue.js.max_requests_timeout}")
private long maxRequestsTimeout; private long maxInvokeRequestsTimeout;
@Value("${queue.js.max_exec_requests_timeout:2000}") @Value("${queue.js.max_exec_requests_timeout:2000}")
private long maxExecRequestsTimeout; private long maxExecRequestsTimeout;
@ -69,37 +72,34 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Value("${js.remote.max_errors}") @Value("${js.remote.max_errors}")
private int maxErrors; private int maxErrors;
@Getter
@Value("${js.remote.max_black_list_duration_sec:60}") @Value("${js.remote.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec; private int maxBlackListDurationSec;
@Getter
@Value("${js.remote.stats.enabled:false}") @Value("${js.remote.stats.enabled:false}")
private boolean statsEnabled; private boolean statsEnabled;
private final AtomicInteger queuePushedMsgs = new AtomicInteger(0);
private final AtomicInteger queueInvokeMsgs = new AtomicInteger(0);
private final AtomicInteger queueEvalMsgs = new AtomicInteger(0);
private final AtomicInteger queueFailedMsgs = new AtomicInteger(0);
private final AtomicInteger queueTimeoutMsgs = new AtomicInteger(0);
private final ExecutorService callbackExecutor = Executors.newFixedThreadPool( private final ExecutorService callbackExecutor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(), ThingsBoardThreadFactory.forName("js-executor-remote-callback")); Runtime.getRuntime().availableProcessors(), ThingsBoardThreadFactory.forName("js-executor-remote-callback"));
public RemoteJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) { public RemoteJsInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageClient) {
super(apiUsageStateService, apiUsageClient); super(apiUsageStateClient, apiUsageClient);
}
@Override
protected Executor getCallbackExecutor() {
return callbackExecutor;
}
@Override
protected String getStatsName() {
return "Queue JS Invoke Stats";
} }
@Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}") @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
public void printStats() { public void printStats() {
if (statsEnabled) { super.printStats();
int pushedMsgs = queuePushedMsgs.getAndSet(0);
int invokeMsgs = queueInvokeMsgs.getAndSet(0);
int evalMsgs = queueEvalMsgs.getAndSet(0);
int failed = queueFailedMsgs.getAndSet(0);
int timedOut = queueTimeoutMsgs.getAndSet(0);
if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) {
log.info("Queue JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut);
}
}
} }
@Autowired @Autowired
@ -110,7 +110,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@PostConstruct @PostConstruct
public void init() { public void init() {
super.init(maxRequestsTimeout); super.init();
requestTemplate.init(); requestTemplate.init();
} }
@ -123,25 +123,25 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
} }
@Override @Override
protected ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String scriptBody) { protected ListenableFuture<UUID> doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody) {
JsInvokeProtos.JsCompileRequest jsRequest = JsInvokeProtos.JsCompileRequest.newBuilder() JsInvokeProtos.JsCompileRequest jsRequest = JsInvokeProtos.JsCompileRequest.newBuilder()
.setScriptHash(scriptHash) .setScriptHash(jsInfo.getHash())
.setFunctionName(functionName) .setFunctionName(jsInfo.getFunctionName())
.setScriptBody(scriptBody).build(); .setScriptBody(scriptBody).build();
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder() JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setCompileRequest(jsRequest) .setCompileRequest(jsRequest)
.build(); .build();
log.trace("Post compile request for scriptId [{}] (hash: {})", scriptId, scriptHash); log.trace("Post compile request for scriptId [{}] (hash: {})", scriptId, jsInfo.getHash());
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = sendJsRequest(UUID.randomUUID(), jsRequestWrapper, maxEvalRequestsTimeout, queueEvalMsgs); ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
return Futures.transform(future, response -> { return Futures.transform(future, response -> {
JsInvokeProtos.JsCompileResponse compilationResult = response.getValue().getCompileResponse(); JsInvokeProtos.JsCompileResponse compilationResult = response.getValue().getCompileResponse();
if (compilationResult.getSuccess()) { if (compilationResult.getSuccess()) {
scriptsLock.lock(); scriptsLock.lock();
try { try {
scriptIdToNameAndHashMap.put(scriptId, Pair.of(functionName, scriptHash)); scriptInfoMap.put(scriptId, jsInfo);
scriptHashToBodysMap.put(scriptHash, scriptBody); scriptHashToBodysMap.put(jsInfo.getHash(), scriptBody);
} finally { } finally {
scriptsLock.unlock(); scriptsLock.unlock();
} }
@ -149,129 +149,111 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
} else { } else {
log.debug("[{}] (hash: {}) Failed to compile script due to [{}]: {}", scriptId, compilationResult.getScriptHash(), log.debug("[{}] (hash: {}) Failed to compile script due to [{}]: {}", scriptId, compilationResult.getScriptHash(),
compilationResult.getErrorCode().name(), compilationResult.getErrorDetails()); compilationResult.getErrorCode().name(), compilationResult.getErrorDetails());
throw new RuntimeException(compilationResult.getErrorDetails()); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, new RuntimeException(compilationResult.getErrorDetails()));
} }
}, callbackExecutor); }, callbackExecutor);
} }
@Override @Override
protected String constructFunctionName(UUID scriptId, String scriptHash) { protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, Object[] args) {
return "invokeInternal_" + scriptHash; var scriptHash = jsInfo.getHash();
}
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args) {
log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptHash, maxRequestsTimeout);
String scriptBody = scriptHashToBodysMap.get(scriptHash); String scriptBody = scriptHashToBodysMap.get(scriptHash);
if (scriptBody == null) { if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for script hash [" + scriptHash + "] (script id: [" + scriptId + "])")); return Futures.immediateFailedFuture(new RuntimeException("No script body found for script hash [" + scriptHash + "] (script id: [" + scriptId + "])"));
} }
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = buildJsInvokeRequest(scriptHash, functionName, args, false, null);
StopWatch stopWatch = new StopWatch(); JsInvokeProtos.RemoteJsRequest jsRequestWrapper = buildJsInvokeRequest(jsInfo, args, false, null);
stopWatch.start();
StopWatch stopWatch;
if (log.isTraceEnabled()) {
stopWatch = new StopWatch();
stopWatch.start();
} else {
stopWatch = null;
}
UUID requestKey = UUID.randomUUID(); UUID requestKey = UUID.randomUUID();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = sendJsRequest(requestKey, jsRequestWrapper, maxRequestsTimeout, queueInvokeMsgs); ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(requestKey, jsRequestWrapper));
return Futures.transformAsync(future, response -> { return Futures.transformAsync(future, response -> {
stopWatch.stop(); if (log.isTraceEnabled()) {
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey()); stopWatch.stop();
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
}
JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse(); JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) { if (invokeResult.getSuccess()) {
return Futures.immediateFuture(invokeResult.getResult()); return Futures.immediateFuture(invokeResult.getResult());
} else { } else {
return handleInvokeError(requestKey, scriptId, scriptHash, invokeResult.getErrorCode(), return handleInvokeError(requestKey, scriptId, jsInfo, invokeResult.getErrorCode(), invokeResult.getErrorDetails(), scriptBody, args);
invokeResult.getErrorDetails(), functionName, args, scriptBody);
} }
}, callbackExecutor); }, callbackExecutor);
} }
private ListenableFuture<Object> handleInvokeError(UUID requestKey, UUID scriptId, String scriptHash, @NotNull
JsInvokeErrorCode errorCode, String errorDetails, private JsInvokeProtos.RemoteJsRequest buildJsInvokeRequest(JsScriptInfo jsInfo, Object[] args, boolean includeScriptBody, String scriptBody) {
String functionName, Object[] args, String scriptBody) { JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder()
.setScriptHash(jsInfo.getHash())
.setFunctionName(jsInfo.getFunctionName())
.setTimeout((int) maxExecRequestsTimeout);
if (includeScriptBody) {
jsRequestBuilder.setScriptBody(scriptBody);
}
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
}
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setInvokeRequest(jsRequestBuilder.build())
.build();
return jsRequestWrapper;
}
private ListenableFuture<Object> handleInvokeError(UUID requestKey, UUID scriptId, JsScriptInfo jsInfo,
JsInvokeProtos.JsInvokeErrorCode errorCode, String errorDetails,
String scriptBody, Object[] args) {
final RuntimeException e = new RuntimeException(errorDetails);
log.debug("[{}] Failed to invoke function due to [{}]: {}", scriptId, errorCode.name(), errorDetails); log.debug("[{}] Failed to invoke function due to [{}]: {}", scriptId, errorCode.name(), errorDetails);
RuntimeException e = new RuntimeException(errorDetails); if (JsInvokeProtos.JsInvokeErrorCode.TIMEOUT_ERROR.equals(errorCode)) {
if (JsInvokeErrorCode.TIMEOUT_ERROR.equals(errorCode)) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.TIMEOUT, scriptBody, new TimeoutException());
onScriptExecutionError(scriptId, e, scriptBody); } else if (JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR.equals(errorCode)) {
queueTimeoutMsgs.incrementAndGet(); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
} else if (JsInvokeErrorCode.COMPILATION_ERROR.equals(errorCode)) { } else if (JsInvokeProtos.JsInvokeErrorCode.NOT_FOUND_ERROR.equals(errorCode)) {
onScriptExecutionError(scriptId, e, scriptBody);
} else if (JsInvokeErrorCode.NOT_FOUND_ERROR.equals(errorCode)) {
log.debug("[{}] Remote JS executor couldn't find the script", scriptId); log.debug("[{}] Remote JS executor couldn't find the script", scriptId);
if (scriptBody != null) { if (scriptBody != null) {
JsInvokeProtos.RemoteJsRequest invokeRequestWithScriptBody = buildJsInvokeRequest(scriptHash, functionName, args, true, scriptBody); JsInvokeProtos.RemoteJsRequest invokeRequestWithScriptBody = buildJsInvokeRequest(jsInfo, args, true, scriptBody);
log.debug("[{}] Sending invoke request again with script body", scriptId); log.debug("[{}] Sending invoke request again with script body", scriptId);
return Futures.transformAsync(sendJsRequest(requestKey, invokeRequestWithScriptBody, maxRequestsTimeout, queueInvokeMsgs), r -> { ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(requestKey, invokeRequestWithScriptBody));
JsInvokeProtos.JsInvokeResponse result = r.getValue().getInvokeResponse(); return Futures.transformAsync(future, response -> {
JsInvokeProtos.JsInvokeResponse result = response.getValue().getInvokeResponse();
if (result.getSuccess()) { if (result.getSuccess()) {
return Futures.immediateFuture(result.getResult()); return Futures.immediateFuture(result.getResult());
} else { } else {
return handleInvokeError(requestKey, scriptId, scriptHash, result.getErrorCode(), return handleInvokeError(requestKey, scriptId, jsInfo, result.getErrorCode(), result.getErrorDetails(), null, args);
result.getErrorDetails(), functionName, args, null);
} }
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
} }
queueFailedMsgs.incrementAndGet(); throw new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, scriptBody, e);
return Futures.immediateFailedFuture(e);
}
private ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> sendJsRequest(UUID requestKey, JsInvokeProtos.RemoteJsRequest jsRequestWrapper,
long maxRequestsTimeout, AtomicInteger msgsCounter) {
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(requestKey, jsRequestWrapper));
if (maxRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
queuePushedMsgs.incrementAndGet();
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse> result) {
msgsCounter.incrementAndGet();
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
queueTimeoutMsgs.incrementAndGet();
}
queueFailedMsgs.incrementAndGet();
}
}, callbackExecutor);
return future;
}
private JsInvokeProtos.RemoteJsRequest buildJsInvokeRequest(String scriptHash, String functionName, Object[] args, boolean includeScriptBody, String scriptBody) {
JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder()
.setScriptHash(scriptHash)
.setFunctionName(functionName)
.setTimeout((int) maxExecRequestsTimeout);
if (includeScriptBody) jsRequestBuilder.setScriptBody(scriptBody);
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
}
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setInvokeRequest(jsRequestBuilder.build())
.build();
return jsRequestWrapper;
} }
@Override @Override
protected void doRelease(UUID scriptId, String scriptHash, String functionName) throws Exception { protected void doRelease(UUID scriptId, JsScriptInfo jsInfo) throws Exception {
if (scriptIdToNameAndHashMap.values().stream().map(Pair::getSecond).anyMatch(hash -> hash.equals(scriptHash))) { String scriptHash = jsInfo.getHash();
if (scriptInfoMap.values().stream().map(JsScriptInfo::getHash).anyMatch(hash -> hash.equals(scriptHash))) {
return; return;
} }
JsInvokeProtos.JsReleaseRequest jsRequest = JsInvokeProtos.JsReleaseRequest.newBuilder() JsInvokeProtos.JsReleaseRequest jsRequest = JsInvokeProtos.JsReleaseRequest.newBuilder()
.setScriptHash(scriptHash) .setScriptHash(scriptHash)
.setFunctionName(functionName).build(); .setFunctionName(jsInfo.getFunctionName()).build();
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder() JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setReleaseRequest(jsRequest) .setReleaseRequest(jsRequest)
.build(); .build();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper)); ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
if (maxRequestsTimeout > 0) { if (getMaxInvokeRequestsTimeout() > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); future = Futures.withTimeout(future, getMaxInvokeRequestsTimeout(), TimeUnit.MILLISECONDS, timeoutExecutorService);
} }
JsInvokeProtos.RemoteJsResponse response = future.get().getValue(); JsInvokeProtos.RemoteJsResponse response = future.get().getValue();
@ -279,7 +261,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
if (releaseResponse.getSuccess()) { if (releaseResponse.getSuccess()) {
scriptsLock.lock(); scriptsLock.lock();
try { try {
if (scriptIdToNameAndHashMap.values().stream().map(Pair::getSecond).noneMatch(h -> h.equals(scriptHash))) { if (scriptInfoMap.values().stream().map(JsScriptInfo::getHash).noneMatch(hash -> hash.equals(scriptHash))) {
scriptHashToBodysMap.remove(scriptHash); scriptHashToBodysMap.remove(scriptHash);
} }
} finally { } finally {
@ -290,9 +272,13 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
} }
} }
@Override protected String constructFunctionName(UUID scriptId, String scriptHash) {
protected long getMaxBlacklistDuration() { return "invokeInternal_" + scriptHash;
return TimeUnit.SECONDS.toMillis(maxBlackListDurationSec); }
protected String getScriptHash(UUID scriptId) {
JsScriptInfo jsScriptInfo = scriptInfoMap.get(scriptId);
return jsScriptInfo != null ? jsScriptInfo.getHash() : null;
} }
} }

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

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

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

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

121
common/script/script-api/pom.xml

@ -0,0 +1,121 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>3.4.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>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>mvel2</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>

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

@ -0,0 +1,288 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api;
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.springframework.data.util.Pair;
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
common/script/script-api/src/main/java/org/thingsboard/script/api/BlockedScriptInfo.java

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

2
application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java → common/script/script-api/src/main/java/org/thingsboard/script/api/RuleNodeScriptFactory.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.script.api;
public class RuleNodeScriptFactory { public class RuleNodeScriptFactory {

11
application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java → common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptInvokeService.java

@ -13,20 +13,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.script.api;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import java.util.UUID; import java.util.UUID;
public interface JsInvokeService { public interface ScriptInvokeService {
ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames); ListenableFuture<UUID> eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args); ListenableFuture<Object> invokeScript(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId); ListenableFuture<Void> release(UUID scriptId);
ScriptLanguage getLanguage();
} }

19
application/src/main/java/org/thingsboard/server/service/script/JsStatCallback.java → common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptStatCallback.java

@ -13,34 +13,35 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.script.api;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@AllArgsConstructor @AllArgsConstructor
public class JsStatCallback<T> implements FutureCallback<T> { public class ScriptStatCallback<T> implements FutureCallback<T> {
private final AtomicInteger jsSuccessMsgs;
private final AtomicInteger jsTimeoutMsgs;
private final AtomicInteger jsFailedMsgs;
private final AtomicInteger successMsgs;
private final AtomicInteger timeoutMsgs;
private final AtomicInteger failedMsgs;
@Override @Override
public void onSuccess(@Nullable T result) { public void onSuccess(@Nullable T result) {
jsSuccessMsgs.incrementAndGet(); successMsgs.incrementAndGet();
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
jsTimeoutMsgs.incrementAndGet(); timeoutMsgs.incrementAndGet();
} else { } else {
jsFailedMsgs.incrementAndGet(); failedMsgs.incrementAndGet();
} }
} }
} }

4
application/src/main/java/org/thingsboard/server/service/script/JsScriptType.java → common/script/script-api/src/main/java/org/thingsboard/script/api/ScriptType.java

@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.script.api;
public enum JsScriptType { public enum ScriptType {
RULE_NODE_SCRIPT RULE_NODE_SCRIPT
} }

40
common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api;
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
common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptExecutionTask.java

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

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

@ -0,0 +1,106 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import 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.springframework.data.util.Pair;
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
common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsInvokeService.java

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

31
common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsScriptExecutionTask.java

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

15
application/src/main/java/org/thingsboard/server/service/script/JsInvokeResponse.java → common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsScriptInfo.java

@ -13,17 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.script; package org.thingsboard.script.api.js;
import java.util.List; import lombok.Data;
/** @Data
* Created by ashvayka on 25.09.18. public class JsScriptInfo {
*/
public class JsInvokeResponse {
private String scriptId; private final String hash;
private String scriptBody; private final String functionName;
private List<String> args;
} }

179
common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java

@ -0,0 +1,179 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.js;
import 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
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
public void stop() {
super.stop();
if (monitorExecutorService != null) {
monitorExecutorService.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;");
}
}
}

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

@ -0,0 +1,181 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import com.google.common.util.concurrent.Futures;
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.ExecutionContext;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.SandboxedParserContext;
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.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
@Slf4j
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "true", matchIfMissing = true)
@Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>();
private SandboxedParserConfiguration parserConfig;
private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s");
@Getter
@Value("${mvel.max_total_args_size:100000}")
private long maxTotalArgsSize;
@Getter
@Value("${mvel.max_result_size:300000}")
private long maxResultSize;
@Getter
@Value("${mvel.max_script_body_size:50000}")
private long maxScriptBodySize;
@Getter
@Value("${mvel.max_errors:3}")
private int maxErrors;
@Getter
@Value("${mvel.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec;
@Getter
@Value("${mvel.max_requests_timeout:0}")
private long maxInvokeRequestsTimeout;
@Getter
@Value("${mvel.stats.enabled:false}")
private boolean statsEnabled;
@Value("${mvel.thread_pool_size:50}")
private int threadPoolSize;
@Value("${mvel.max_memory_limit_mb:8}")
private long maxMemoryLimitMb;
private ListeningExecutorService executor;
protected DefaultMvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
super(apiUsageStateClient, apiUsageReportClient);
}
@Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}")
public void printStats() {
super.printStats();
}
@SneakyThrows
@PostConstruct
public void init() {
super.init();
OptimizerFactory.setDefaultOptimizer(OptimizerFactory.SAFE_REFLECTIVE);
parserConfig = new SandboxedParserConfiguration();
parserConfig.addImport("JSON", TbJson.class);
TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor"));
}
@PreDestroy
public void destroy() {
if (executor != null) {
executor.shutdownNow();
}
}
@Override
protected String getStatsName() {
return "MVEL Scripts Stats";
}
@Override
protected Executor getCallbackExecutor() {
return MoreExecutors.directExecutor();
}
@Override
protected boolean isScriptPresent(UUID scriptId) {
return scriptMap.containsKey(scriptId);
}
@Override
protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) {
if (NEW_KEYWORD_PATTERN.matcher(scriptBody).matches()) {
//TODO: output line number and char pos.
return Futures.immediateFailedFuture(new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody,
new IllegalArgumentException("Keyword 'new' is forbidden!")));
}
return executor.submit(() -> {
try {
Serializable compiledScript = MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig));
MvelScript script = new MvelScript(compiledScript, scriptBody, argNames);
scriptMap.put(scriptId, script);
return scriptId;
} catch (Exception e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
}
});
}
@Override
protected MvelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
ExecutionContext executionContext = new ExecutionContext(maxMemoryLimitMb * 1024 * 1024);
return new MvelScriptExecutionTask(executionContext, executor.submit(() -> {
MvelScript script = scriptMap.get(scriptId);
if (script == null) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!"));
}
try {
return MVEL.executeTbExpression(script.getCompiledScript(), 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) throws Exception {
scriptMap.remove(scriptId);
}
}

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

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

41
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
public class MvelScript {
private final Serializable compiledScript;
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;
}
}

38
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScriptExecutionTask.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.Getter;
import org.mvel2.ExecutionContext;
import org.thingsboard.script.api.TbScriptExecutionTask;
public class MvelScriptExecutionTask extends TbScriptExecutionTask {
private final ExecutionContext context;
public MvelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) {
super(resultFuture);
this.context = context;
}
@Override
public void stop(){
context.stop();
}
}

61
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbJson.java

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

87
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbUtils.java

@ -0,0 +1,87 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.util.MethodStub;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.List;
public class TbUtils {
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("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)));
}
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 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 (int i = 0; i < bytes.length; i++) {
list.add(bytes[i]);
}
return list;
}
}

4
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/TbApiUsageClient.java → common/stats/src/main/java/org/thingsboard/server/common/stats/TbApiUsageReportClient.java

@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.queue.usagestats; package org.thingsboard.server.common.stats;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
public interface TbApiUsageClient { public interface TbApiUsageReportClient {
void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value); void report(TenantId tenantId, CustomerId customerId, ApiUsageRecordKey key, long value);

25
common/stats/src/main/java/org/thingsboard/server/common/stats/TbApiUsageStateClient.java

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

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -57,6 +57,7 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.common.transport.DeviceDeletedEvent; import org.thingsboard.server.common.transport.DeviceDeletedEvent;
import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
@ -94,7 +95,6 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.provider.TbTransportQueueFactory; import org.thingsboard.server.queue.provider.TbTransportQueueFactory;
import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbTransportComponent; import org.thingsboard.server.queue.util.TbTransportComponent;
@ -157,7 +157,7 @@ public class DefaultTransportService implements TransportService {
@Autowired @Autowired
@Lazy @Lazy
private TbApiUsageClient apiUsageClient; private TbApiUsageReportClient apiUsageClient;
private final Map<String, Number> statsMap = new LinkedHashMap<>(); private final Map<String, Number> statsMap = new LinkedHashMap<>();
private final Gson gson = new Gson(); private final Gson gson = new Gson();

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

@ -24,6 +24,8 @@ import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -116,6 +118,14 @@ public class JacksonUtil {
} }
} }
public static <T> T treeToValue(JsonNode node, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(node, clazz);
} catch (IOException e) {
throw new IllegalArgumentException("Can't convert value: " + node.toString(), e);
}
}
public static JsonNode toJsonNode(String value) { public static JsonNode toJsonNode(String value) {
if (value == null || value.isEmpty()) { if (value == null || value.isEmpty()) {
return null; return null;
@ -127,14 +137,6 @@ public class JacksonUtil {
} }
} }
public static <T> T treeToValue(JsonNode node, Class<T> clazz) {
try {
return OBJECT_MAPPER.treeToValue(node, clazz);
} catch (IOException e) {
throw new IllegalArgumentException("Can't convert value: " + node.toString(), e);
}
}
public static ObjectNode newObjectNode() { public static ObjectNode newObjectNode() {
return OBJECT_MAPPER.createObjectNode(); return OBJECT_MAPPER.createObjectNode();
} }
@ -212,4 +214,20 @@ public class JacksonUtil {
} }
} }
} }
public static void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) {
if (kvEntry.getDataType() == DataType.BOOLEAN) {
kvEntry.getBooleanValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.DOUBLE) {
kvEntry.getDoubleValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.LONG) {
kvEntry.getLongValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.JSON) {
if (kvEntry.getJsonValue().isPresent()) {
entityNode.set(kvEntry.getKey(), JacksonUtil.toJsonNode(kvEntry.getJsonValue().get()));
}
} else {
entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
}
}
} }

7
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java

@ -47,6 +47,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -219,12 +220,14 @@ public class DefaultGitRepositoryService implements GitRepositoryService {
@Override @Override
public void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception { public void testRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString()); Path testDirectory = Path.of(repositoriesFolder, "repo-test-" + UUID.randomUUID());
GitRepository.test(settings, repositoryDirectory.toFile()); GitRepository.test(settings, testDirectory.toFile());
} }
@Override @Override
public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception { public void initRepository(TenantId tenantId, RepositorySettings settings) throws Exception {
testRepository(tenantId, settings);
clearRepository(tenantId); clearRepository(tenantId);
log.debug("[{}] Init tenant repository started.", tenantId); log.debug("[{}] Init tenant repository started.", tenantId);
Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString()); Path repositoryDirectory = Path.of(repositoriesFolder, tenantId.getId().toString());

182
common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java

@ -20,7 +20,9 @@ import com.google.common.collect.Ordering;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils; import lombok.RequiredArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sshd.common.util.security.SecurityUtils; import org.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
@ -49,6 +51,7 @@ import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.sshd.JGitKeyCache; import org.eclipse.jgit.transport.sshd.JGitKeyCache;
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
@ -61,8 +64,8 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.sync.vc.BranchInfo; import org.thingsboard.server.common.data.sync.vc.BranchInfo;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -70,6 +73,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList; import java.util.ArrayList;
@ -78,70 +82,67 @@ import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class GitRepository { public class GitRepository {
private final Git git; private final Git git;
private final AuthHandler authHandler;
@Getter @Getter
private final RepositorySettings settings; private final RepositorySettings settings;
private final CredentialsProvider credentialsProvider;
private final SshdSessionFactory sshSessionFactory;
@Getter @Getter
private final String directory; private final String directory;
private ObjectId headId; private ObjectId headId;
private GitRepository(Git git, RepositorySettings settings, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory, String directory) { private GitRepository(Git git, RepositorySettings settings, AuthHandler authHandler, String directory) {
this.git = git; this.git = git;
this.settings = settings; this.settings = settings;
this.credentialsProvider = credentialsProvider; this.authHandler = authHandler;
this.sshSessionFactory = sshSessionFactory;
this.directory = directory; this.directory = directory;
} }
public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException { public static GitRepository clone(RepositorySettings settings, File directory) throws GitAPIException {
CredentialsProvider credentialsProvider = null;
SshdSessionFactory sshSessionFactory = null;
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
CloneCommand cloneCommand = Git.cloneRepository() CloneCommand cloneCommand = Git.cloneRepository()
.setURI(settings.getRepositoryUri()) .setURI(settings.getRepositoryUri())
.setDirectory(directory) .setDirectory(directory)
.setNoCheckout(true); .setNoCheckout(true);
configureTransportCommand(cloneCommand, credentialsProvider, sshSessionFactory); AuthHandler authHandler = AuthHandler.createFor(settings, directory);
authHandler.configureCommand(cloneCommand);
Git git = cloneCommand.call(); Git git = cloneCommand.call();
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath()); return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
} }
public static GitRepository open(File directory, RepositorySettings settings) throws IOException { public static GitRepository open(File directory, RepositorySettings settings) throws IOException {
Git git = Git.open(directory); Git git = Git.open(directory);
CredentialsProvider credentialsProvider = null; AuthHandler authHandler = AuthHandler.createFor(settings, directory);
SshdSessionFactory sshSessionFactory = null; return new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
return new GitRepository(git, settings, credentialsProvider, sshSessionFactory, directory.getAbsolutePath());
} }
public static void test(RepositorySettings settings, File directory) throws GitAPIException { public static void test(RepositorySettings settings, File directory) throws Exception {
CredentialsProvider credentialsProvider = null; AuthHandler authHandler = AuthHandler.createFor(settings, directory);
SshdSessionFactory sshSessionFactory = null; if (settings.isReadOnly()) {
if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) { LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword()); authHandler.configureCommand(lsRemoteCommand);
} else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) { lsRemoteCommand.call();
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory); } else {
Files.createDirectories(directory.toPath());
try {
Git git = Git.init().setDirectory(directory).call();
GitRepository repository = new GitRepository(git, settings, authHandler, directory.getAbsolutePath());
repository.execute(repository.git.remoteAdd()
.setName("origin")
.setUri(new URIish(settings.getRepositoryUri())));
repository.push("", UUID.randomUUID().toString()); // trying to delete non-existing branch on remote repo
} finally {
try {
FileUtils.forceDelete(directory);
} catch (Exception ignored) {}
}
} }
LsRemoteCommand lsRemoteCommand = Git.lsRemoteRepository().setRemote(settings.getRepositoryUri());
configureTransportCommand(lsRemoteCommand, credentialsProvider, sshSessionFactory);
lsRemoteCommand.call();
} }
public void fetch() throws GitAPIException { public void fetch() throws GitAPIException {
@ -363,12 +364,12 @@ public class GitRepository {
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException { private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException {
if (command instanceof TransportCommand) { if (command instanceof TransportCommand) {
configureTransportCommand((TransportCommand) command, credentialsProvider, sshSessionFactory); authHandler.configureCommand((TransportCommand) command);
} }
return command.call(); return command.call();
} }
private static Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> { private static final Function<PageLink, Comparator<RevCommit>> revCommitComparatorFunction = pageLink -> {
SortOrder sortOrder = pageLink.getSortOrder(); SortOrder sortOrder = pageLink.getSortOrder();
if (sortOrder != null if (sortOrder != null
&& sortOrder.getProperty().equals("timestamp") && sortOrder.getProperty().equals("timestamp")
@ -405,59 +406,76 @@ public class GitRepository {
return new PageData<>(data, totalPages, totalElements, hasNext); return new PageData<>(data, totalPages, totalElements, hasNext);
} }
private static void configureTransportCommand(TransportCommand transportCommand, CredentialsProvider credentialsProvider, SshdSessionFactory sshSessionFactory) { @RequiredArgsConstructor
if (credentialsProvider != null) { private static class AuthHandler {
transportCommand.setCredentialsProvider(credentialsProvider); private final CredentialsProvider credentialsProvider;
} private final SshdSessionFactory sshSessionFactory;
if (sshSessionFactory != null) {
transportCommand.setTransportConfigCallback(transport -> { protected static AuthHandler createFor(RepositorySettings settings, File directory) {
if (transport instanceof SshTransport) { CredentialsProvider credentialsProvider = null;
SshTransport sshTransport = (SshTransport) transport; SshdSessionFactory sshSessionFactory = null;
sshTransport.setSshSessionFactory(sshSessionFactory); if (RepositoryAuthMethod.USERNAME_PASSWORD.equals(settings.getAuthMethod())) {
} credentialsProvider = newCredentialsProvider(settings.getUsername(), settings.getPassword());
}); } else if (RepositoryAuthMethod.PRIVATE_KEY.equals(settings.getAuthMethod())) {
sshSessionFactory = newSshdSessionFactory(settings.getPrivateKey(), settings.getPrivateKeyPassword(), directory);
}
return new AuthHandler(credentialsProvider, sshSessionFactory);
} }
}
private static CredentialsProvider newCredentialsProvider(String username, String password) { protected void configureCommand(TransportCommand command) {
return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password); if (credentialsProvider != null) {
} command.setCredentialsProvider(credentialsProvider);
}
if (sshSessionFactory != null) {
command.setTransportConfigCallback(transport -> {
if (transport instanceof SshTransport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
}
});
}
}
private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) { private static CredentialsProvider newCredentialsProvider(String username, String password) {
SshdSessionFactory sshSessionFactory = null; return new UsernamePasswordCredentialsProvider(username, password == null ? "" : password);
if (StringUtils.isNotBlank(privateKey)) { }
Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password);
sshSessionFactory = new SshdSessionFactoryBuilder()
.setPreferredAuthentications("publickey")
.setDefaultKeysProvider(file -> keyPairs)
.setHomeDirectory(directory)
.setSshDirectory(directory)
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() {
@Override
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) {
return Collections.emptyList();
}
@Override private static SshdSessionFactory newSshdSessionFactory(String privateKey, String password, File directory) {
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) { SshdSessionFactory sshSessionFactory = null;
return true; if (StringUtils.isNotBlank(privateKey)) {
} Iterable<KeyPair> keyPairs = loadKeyPairs(privateKey, password);
}) sshSessionFactory = new SshdSessionFactoryBuilder()
.build(new JGitKeyCache()); .setPreferredAuthentications("publickey")
.setDefaultKeysProvider(file -> keyPairs)
.setHomeDirectory(directory)
.setSshDirectory(directory)
.setServerKeyDatabase((file, file2) -> new ServerKeyDatabase() {
@Override
public List<PublicKey> lookup(String connectAddress, InetSocketAddress remoteAddress, Configuration config) {
return Collections.emptyList();
}
@Override
public boolean accept(String connectAddress, InetSocketAddress remoteAddress, PublicKey serverKey, Configuration config, CredentialsProvider provider) {
return true;
}
})
.build(new JGitKeyCache());
}
return sshSessionFactory;
} }
return sshSessionFactory;
}
private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) { private static Iterable<KeyPair> loadKeyPairs(String privateKeyContent, String password) {
Iterable<KeyPair> keyPairs = null; Iterable<KeyPair> keyPairs = null;
try { try {
keyPairs = SecurityUtils.loadKeyPairIdentities(null, keyPairs = SecurityUtils.loadKeyPairIdentities(null,
null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password); null, new ByteArrayInputStream(privateKeyContent.getBytes()), (session, resourceKey, retryIndex) -> password);
} catch (Exception e) {} } catch (Exception e) {}
if (keyPairs == null) { if (keyPairs == null) {
throw new IllegalArgumentException("Failed to load ssh private key"); throw new IllegalArgumentException("Failed to load ssh private key");
}
return keyPairs;
} }
return keyPairs;
} }
private static class NoMergesAndCommitMessageFilter extends RevFilter { private static class NoMergesAndCommitMessageFilter extends RevFilter {

45
dao/src/main/java/org/thingsboard/server/dao/aspect/SqlDaoCallsAspect.java

@ -21,7 +21,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
@ -29,9 +28,11 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.exception.JDBCConnectionException; import org.hibernate.exception.JDBCConnectionException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import java.util.Arrays; import java.util.Arrays;
@ -46,6 +47,8 @@ import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.join;
@Aspect @Aspect
@ConditionalOnProperty(prefix = "sql", value = "log_tenant_stats", havingValue = "true") @ConditionalOnProperty(prefix = "sql", value = "log_tenant_stats", havingValue = "true")
@Component @Component
@ -55,6 +58,12 @@ public class SqlDaoCallsAspect {
private final Set<String> invalidTenantDbCallMethods = ConcurrentHashMap.newKeySet(); private final Set<String> invalidTenantDbCallMethods = ConcurrentHashMap.newKeySet();
private final ConcurrentMap<TenantId, DbCallStats> statsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, DbCallStats> statsMap = new ConcurrentHashMap<>();
@Value("${sql.batch_sort:true}")
private boolean batchSortEnabled;
private static final String DEADLOCK_DETECTED_ERROR = "deadlock detected";
@Scheduled(initialDelayString = "${sql.log_tenant_stats_interval_ms:60000}", @Scheduled(initialDelayString = "${sql.log_tenant_stats_interval_ms:60000}",
fixedDelayString = "${sql.log_tenant_stats_interval_ms:60000}") fixedDelayString = "${sql.log_tenant_stats_interval_ms:60000}")
public void printStats() { public void printStats() {
@ -135,7 +144,7 @@ public class SqlDaoCallsAspect {
@SuppressWarnings({"rawtypes", "unchecked"}) @SuppressWarnings({"rawtypes", "unchecked"})
@Around("@within(org.thingsboard.server.dao.util.SqlDao)") @Around("@within(org.thingsboard.server.dao.util.SqlDao)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { public Object handleSqlCall(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); MethodSignature signature = (MethodSignature) joinPoint.getSignature();
var methodName = signature.toShortString(); var methodName = signature.toShortString();
if (invalidTenantDbCallMethods.contains(methodName)) { if (invalidTenantDbCallMethods.contains(methodName)) {
@ -155,29 +164,47 @@ public class SqlDaoCallsAspect {
new FutureCallback<>() { new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Object result) { public void onSuccess(@Nullable Object result) {
logTenantMethodExecution(tenantId, methodName, true, startTime, null); reportSuccessfulMethodExecution(tenantId, methodName, startTime);
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
logTenantMethodExecution(tenantId, methodName, false, startTime, t); reportFailedMethodExecution(tenantId, methodName, startTime, t, joinPoint);
} }
}, },
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} else { } else {
logTenantMethodExecution(tenantId, methodName, true, startTime, null); reportSuccessfulMethodExecution(tenantId, methodName, startTime);
} }
return result; return result;
} catch (Throwable t) { } catch (Throwable t) {
logTenantMethodExecution(tenantId, methodName, false, startTime, t); reportFailedMethodExecution(tenantId, methodName, startTime, t, joinPoint);
throw t; throw t;
} }
} }
private void logTenantMethodExecution(TenantId tenantId, String method, boolean success, long startTime, Throwable t) { private void reportFailedMethodExecution(TenantId tenantId, String method, long startTime, Throwable t, ProceedingJoinPoint joinPoint) {
if (!success && ExceptionUtils.indexOfThrowable(t, JDBCConnectionException.class) >= 0) { if (t != null) {
return; if (ExceptionUtils.indexOfThrowable(t, JDBCConnectionException.class) >= 0) {
return;
}
if (StringUtils.containedByAny(DEADLOCK_DETECTED_ERROR, ExceptionUtils.getRootCauseMessage(t), ExceptionUtils.getMessage(t))) {
if (!batchSortEnabled) {
log.warn("Deadlock was detected for method {} (tenant: {}). You might need to enable 'sql.batch_sort' option.", method, tenantId);
} else {
log.error("Deadlock was detected for method {} (tenant: {}). Arguments passed: \n{}\n The error: ",
method, tenantId, join(joinPoint.getArgs(), System.lineSeparator()), t);
}
}
} }
reportMethodExecution(tenantId, method, false, startTime);
}
private void reportSuccessfulMethodExecution(TenantId tenantId, String method, long startTime) {
reportMethodExecution(tenantId, method, true, startTime);
}
private void reportMethodExecution(TenantId tenantId, String method, boolean success, long startTime) {
statsMap.computeIfAbsent(tenantId, DbCallStats::new) statsMap.computeIfAbsent(tenantId, DbCallStats::new)
.onMethodCall(method, success, System.currentTimeMillis() - startTime); .onMethodCall(method, success, System.currentTimeMillis() - startTime);
} }

4
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java

@ -25,6 +25,7 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
import org.thingsboard.server.dao.util.SqlDao;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
@ -35,6 +36,7 @@ import java.util.regex.Pattern;
@Repository @Repository
@Slf4j @Slf4j
@SqlDao
public abstract class AttributeKvInsertRepository { public abstract class AttributeKvInsertRepository {
private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
@ -58,7 +60,7 @@ public abstract class AttributeKvInsertRepository {
@Value("${sql.remove_null_chars:true}") @Value("${sql.remove_null_chars:true}")
private boolean removeNullChars; private boolean removeNullChars;
protected void saveOrUpdate(List<AttributeKvEntity> entities) { public void saveOrUpdate(List<AttributeKvEntity> entities) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() { transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override @Override
protected void doInTransactionWithoutResult(TransactionStatus status) { protected void doInTransactionWithoutResult(TransactionStatus status) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -78,7 +78,7 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Value("${sql.attributes.batch_threads:4}") @Value("${sql.attributes.batch_threads:4}")
private int batchThreads; private int batchThreads;
@Value("${sql.batch_sort:false}") @Value("${sql.batch_sort:true}")
private boolean batchSortEnabled; private boolean batchSortEnabled;
private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue; private TbSqlBlockingQueueWrapper<AttributeKvEntity> queue;

2
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/SqlAttributesInsertRepository.java

@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.attributes;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.util.SqlDao;
@Repository @Repository
@Transactional @Transactional
@SqlDao
public class SqlAttributesInsertRepository extends AttributeKvInsertRepository { public class SqlAttributesInsertRepository extends AttributeKvInsertRepository {
} }

4
dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.event.RuleChainDebugEvent; import org.thingsboard.server.common.data.event.RuleChainDebugEvent;
import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; import org.thingsboard.server.common.data.event.RuleNodeDebugEvent;
import org.thingsboard.server.common.data.event.StatisticsEvent; import org.thingsboard.server.common.data.event.StatisticsEvent;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
@ -45,6 +46,7 @@ import java.util.stream.Collectors;
@Repository @Repository
@Transactional @Transactional
@SqlDao
public class EventInsertRepository { public class EventInsertRepository {
private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE))); private static final ThreadLocal<Pattern> PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
@ -81,7 +83,7 @@ public class EventInsertRepository {
"VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING;"); "VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING;");
} }
protected void save(List<Event> entities) { public void save(List<Event> entities) {
Map<EventType, List<Event>> eventsByType = entities.stream().collect(Collectors.groupingBy(Event::getType, Collectors.toList())); Map<EventType, List<Event>> eventsByType = entities.stream().collect(Collectors.groupingBy(Event::getType, Collectors.toList()));
transactionTemplate.execute(new TransactionCallbackWithoutResult() { transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -106,7 +106,7 @@ public class JpaBaseEventDao implements EventDao {
@Value("${sql.events.batch_threads:3}") @Value("${sql.events.batch_threads:3}")
private int batchThreads; private int batchThreads;
@Value("${sql.batch_sort:false}") @Value("${sql.batch_sort:true}")
private boolean batchSortEnabled; private boolean batchSortEnabled;
private TbSqlBlockingQueueWrapper<Event> queue; private TbSqlBlockingQueueWrapper<Event> queue;

5
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java

@ -60,6 +60,11 @@ public class JpaUserDao extends JpaAbstractSearchTextDao<UserEntity, User> imple
return DaoUtil.getData(userRepository.findByEmail(email)); return DaoUtil.getData(userRepository.findByEmail(email));
} }
@Override
public User findByTenantIdAndEmail(TenantId tenantId, String email) {
return DaoUtil.getData(userRepository.findByTenantIdAndEmail(tenantId.getId(), email));
}
@Override @Override
public PageData<User> findByTenantId(UUID tenantId, PageLink pageLink) { public PageData<User> findByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData( return DaoUtil.toPageData(

4
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java

@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.model.sql.UserEntity; import org.thingsboard.server.dao.model.sql.UserEntity;
@ -32,6 +33,8 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
UserEntity findByEmail(String email); UserEntity findByEmail(String email);
UserEntity findByTenantIdAndEmail(UUID tenantId, String email);
@Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " + @Query("SELECT u FROM UserEntity u WHERE u.tenantId = :tenantId " +
"AND u.customerId = :customerId AND u.authority = :authority " + "AND u.customerId = :customerId AND u.authority = :authority " +
"AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") "AND LOWER(u.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))")
@ -48,4 +51,5 @@ public interface UserRepository extends JpaRepository<UserEntity, UUID> {
Pageable pageable); Pageable pageable);
Long countByTenantId(UUID tenantId); Long countByTenantId(UUID tenantId);
} }

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java

@ -63,7 +63,7 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries
@Value("${sql.timescale.batch_threads:4}") @Value("${sql.timescale.batch_threads:4}")
protected int timescaleBatchThreads; protected int timescaleBatchThreads;
@Value("${sql.batch_sort:false}") @Value("${sql.batch_sort:true}")
protected boolean batchSortEnabled; protected boolean batchSortEnabled;
@Value("${sql.ttl.ts.ts_key_value_ttl:0}") @Value("${sql.ttl.ts.ts_key_value_ttl:0}")

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -94,7 +94,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Value("${sql.ts_latest.batch_threads:4}") @Value("${sql.ts_latest.batch_threads:4}")
private int tsLatestBatchThreads; private int tsLatestBatchThreads;
@Value("${sql.batch_sort:false}") @Value("${sql.batch_sort:true}")
protected boolean batchSortEnabled; protected boolean batchSortEnabled;
@Autowired @Autowired

2
dao/src/main/java/org/thingsboard/server/dao/sqlts/insert/latest/sql/SqlLatestInsertTsRepository.java

@ -24,6 +24,7 @@ import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository; import org.thingsboard.server.dao.sqlts.insert.AbstractInsertRepository;
import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository; import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository;
import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
@ -36,6 +37,7 @@ import java.util.List;
@SqlTsLatestAnyDao @SqlTsLatestAnyDao
@Repository @Repository
@Transactional @Transactional
@SqlDao
public class SqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository { public class SqlLatestInsertTsRepository extends AbstractInsertRepository implements InsertLatestTsRepository {
@Value("${sql.ts_latest.update_by_latest_ts:true}") @Value("${sql.ts_latest.update_by_latest_ts:true}")

10
dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java

@ -42,6 +42,15 @@ public interface UserDao extends Dao<User>, TenantEntityDao {
*/ */
User findByEmail(TenantId tenantId, String email); User findByEmail(TenantId tenantId, String email);
/**
* Find user by tenant id and email.
*
* @param tenantId the tenant id
* @param email the email
* @return the user entity
*/
User findByTenantIdAndEmail(TenantId tenantId, String email);
/** /**
* Find users by tenantId and page link. * Find users by tenantId and page link.
* *
@ -69,4 +78,5 @@ public interface UserDao extends Dao<User>, TenantEntityDao {
* @return the list of user entities * @return the list of user entities
*/ */
PageData<User> findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink); PageData<User> findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink);
} }

8
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -87,6 +87,14 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
} }
} }
@Override
public User findUserByTenantIdAndEmail(TenantId tenantId, String email) {
log.trace("Executing findUserByTenantIdAndEmail [{}][{}]", tenantId, email);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
validateString(email, "Incorrect email " + email);
return userDao.findByTenantIdAndEmail(tenantId, email);
}
@Override @Override
public User findUserById(TenantId tenantId, UserId userId) { public User findUserById(TenantId tenantId, UserId userId) {
log.trace("Executing findUserById [{}]", userId); log.trace("Executing findUserById [{}]", userId);

18
pom.xml

@ -75,8 +75,9 @@
<mail.version>1.6.2</mail.version> <mail.version>1.6.2</mail.version>
<curator.version>4.2.0</curator.version> <curator.version>4.2.0</curator.version>
<zookeeper.version>3.5.5</zookeeper.version> <zookeeper.version>3.5.5</zookeeper.version>
<protobuf.version>3.17.2</protobuf.version> <protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version> <grpc.version>1.42.1</grpc.version>
<mvel.version>2.4.22TB</mvel.version>
<lombok.version>1.18.18</lombok.version> <lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version> <paho.client.version>1.2.4</paho.client.version>
<netty.version>4.1.75.Final</netty.version> <netty.version>4.1.75.Final</netty.version>
@ -969,6 +970,16 @@
<artifactId>coap-server</artifactId> <artifactId>coap-server</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard.common.script</groupId>
<artifactId>script-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.thingsboard.common.script</groupId>
<artifactId>remote-js-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>tools</artifactId> <artifactId>tools</artifactId>
@ -1563,6 +1574,11 @@
<artifactId>grpc-api</artifactId> <artifactId>grpc-api</artifactId>
<version>${grpc.version}</version> <version>${grpc.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>mvel2</artifactId>
<version>${mvel.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId> <artifactId>spring-test</artifactId>

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

@ -31,14 +31,15 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -62,6 +63,7 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -183,6 +185,10 @@ public interface TbContext {
// TODO: Does this changes the message? // TODO: Does this changes the message?
TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action); TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action);
TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<AttributeKvEntry> attributes);
TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<String> keys);
void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId); void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId);
/* /*
@ -261,8 +267,17 @@ public interface TbContext {
SmsSenderFactory getSmsSenderFactory(); SmsSenderFactory getSmsSenderFactory();
/**
* Creates JS Script Engine
* @deprecated
* <p> Use {@link #createScriptEngine} instead.
*
*/
@Deprecated
ScriptEngine createJsScriptEngine(String script, String... argNames); ScriptEngine createJsScriptEngine(String script, String... argNames);
ScriptEngine createScriptEngine(ScriptLanguage scriptLang, String script, String... argNames);
void logJsEvalRequest(); void logJsEvalRequest();
void logJsEvalResponse(); void logJsEvalResponse();
@ -298,4 +313,5 @@ public interface TbContext {
void removeListeners(); void removeListeners();
TenantProfile getTenantProfile(); TenantProfile getTenantProfile();
} }

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

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

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

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

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java

@ -258,7 +258,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
break; break;
case USER: case USER:
UserService userService = ctx.getUserService(); UserService userService = ctx.getUserService();
User user = userService.findUserByEmail(ctx.getTenantId(), entitykey.getEntityName()); User user = userService.findUserByTenantIdAndEmail(ctx.getTenantId(), entitykey.getEntityName());
if (user != null) { if (user != null) {
targetEntity.setEntityId(user.getId()); targetEntity.setEntityId(user.getId());
} }

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

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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save