Browse Source

merge with develop/3.6

pull/8709/head
YevhenBondarenko 3 years ago
parent
commit
1a12d3c395
  1. 4
      application/pom.xml
  2. 22
      application/src/main/data/certs/azure/BaltimoreCyberTrustRoot.crt.pem
  3. 22
      application/src/main/data/certs/azure/DigiCertGlobalRootG2.crt.pem
  4. 16
      application/src/main/data/json/system/widget_bundles/cards.json
  5. 24
      application/src/main/data/json/system/widget_bundles/charts.json
  6. 70
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  7. 6
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  8. 12
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  9. 10
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
  10. 220
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  11. 41
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  12. 9
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
  13. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
  14. 78
      application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java
  15. 5
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  16. 6
      application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
  17. 9
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  18. 126
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  19. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  20. 2
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  21. 162
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  22. 23
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  23. 2
      application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
  24. 56
      application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
  25. 2
      application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
  26. 2
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  27. 87
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  28. 2
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  29. 3
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  30. 71
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  31. 4
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  32. 82
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  33. 6
      application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
  34. 40
      application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java
  35. 38
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  36. 245
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  37. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java
  38. 35
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  39. 5
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  40. 3
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  41. 12
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  42. 2
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  43. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java
  44. 81
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  45. 71
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  46. 42
      application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java
  47. 75
      application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java
  48. 24
      application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java
  49. 32
      application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java
  50. 168
      application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java
  51. 103
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  52. 20
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  53. 3
      application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
  54. 10
      application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
  55. 13
      application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
  56. 102
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  57. 17
      application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java
  58. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java
  59. 45
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  60. 74
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  61. 12
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  62. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java
  63. 44
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java
  64. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
  65. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  66. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
  67. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
  68. 71
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java
  69. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
  70. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  71. 16
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  72. 3
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  73. 14
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  74. 5
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  75. 50
      application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
  76. 3
      application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java
  77. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  78. 25
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  79. 22
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  80. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  81. 6
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java
  82. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java
  83. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
  84. 4
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  85. 4
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  86. 3
      application/src/main/resources/logback.xml
  87. 52
      application/src/main/resources/templates/mail_config_templates.json
  88. 23
      application/src/main/resources/thingsboard.yml
  89. 20
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  90. 45
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  91. 23
      application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
  92. 76
      application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
  93. 251
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  94. 9
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  95. 4
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  96. 10
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  97. 4
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  98. 10
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  99. 40
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  100. 2
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java

4
application/pom.xml

@ -361,6 +361,10 @@
<groupId>com.slack.api</groupId>
<artifactId>slack-api-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
</dependency>
</dependencies>
<build>

22
application/src/main/data/certs/azure/BaltimoreCyberTrustRoot.crt.pem

@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
-----END CERTIFICATE-----

22
application/src/main/data/certs/azure/DigiCertGlobalRootG2.crt.pem

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
MrY=
-----END CERTIFICATE-----

16
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

24
application/src/main/data/json/system/widget_bundles/charts.json

File diff suppressed because one or more lines are too long

70
application/src/main/data/upgrade/3.5.1/schema_update.sql

@ -52,3 +52,73 @@ $$
$$;
-- NOTIFICATION CONFIGS VERSION CONTROL END
-- EDGE EVENTS MIGRATION START
DO
$$
DECLARE table_partition RECORD;
BEGIN
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN
ALTER TABLE edge_event RENAME TO old_edge_event;
CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time);
ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
FROM pg_tables WHERE tablename LIKE 'edge_event_%'
LOOP
EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts);
END LOOP;
ELSE
RAISE NOTICE 'Table old_edge_event already exists, leaving as is';
END IF;
END;
$$;
CREATE TABLE IF NOT EXISTS edge_event (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL,
created_time bigint NOT NULL,
edge_id uuid,
edge_event_type varchar(255),
edge_event_uid varchar(255),
entity_id uuid,
edge_event_action varchar(255),
body varchar(10000000),
tenant_id uuid,
ts bigint NOT NULL
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
$$
DECLARE
p RECORD;
partition_end_ts BIGINT;
BEGIN
FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms
LOOP
partition_end_ts = p.partition_ts + partition_size_ms;
RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' ||
'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
END LOOP;
INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts)
SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts
FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;
-- EDGE EVENTS MIGRATION END
ALTER TABLE resource
ADD COLUMN IF NOT EXISTS etag varchar;
UPDATE resource
SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null;

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

@ -90,7 +90,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
@ -536,6 +536,10 @@ public class ActorSystemContext {
@Getter
private int maxRpcRetries;
@Value("${actors.rule.external.force_ack:false}")
@Getter
private boolean externalNodeForceAck;
@Getter
@Setter
private TbActorSystem actorSystem;

12
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -73,10 +73,14 @@ public class AppActor extends ContextAwareActor {
@Override
protected boolean doProcess(TbActorMsg msg) {
if (!ruleChainsInitialized) {
initTenantActors();
ruleChainsInitialized = true;
if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) {
log.warn("Rule Chains initialized by unexpected message: {}", msg);
if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) {
initTenantActors();
ruleChainsInitialized = true;
} else {
if (!msg.getMsgType().isIgnoreOnStart()) {
log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg);
}
return true;
}
}
switch (msg.getMsgType()) {

10
application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java

@ -59,10 +59,10 @@ public class DeviceActor extends ContextAwareActor {
protected boolean doProcess(TbActorMsg msg) {
switch (msg.getMsgType()) {
case TRANSPORT_TO_DEVICE_ACTOR_MSG:
processor.process(ctx, (TransportToDeviceActorMsgWrapper) msg);
processor.process((TransportToDeviceActorMsgWrapper) msg);
break;
case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processAttributesUpdate(ctx, (DeviceAttributesEventNotificationMsg) msg);
processor.processAttributesUpdate((DeviceAttributesEventNotificationMsg) msg);
break;
case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
processor.processCredentialsUpdate(msg);
@ -74,10 +74,10 @@ public class DeviceActor extends ContextAwareActor {
processor.processRpcRequest(ctx, (ToDeviceRpcRequestActorMsg) msg);
break;
case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
processor.processRpcResponsesFromEdge(ctx, (FromDeviceRpcResponseActorMsg) msg);
processor.processRpcResponsesFromEdge((FromDeviceRpcResponseActorMsg) msg);
break;
case DEVICE_ACTOR_SERVER_SIDE_RPC_TIMEOUT_MSG:
processor.processServerSideRpcTimeout(ctx, (DeviceActorServerSideRpcTimeoutMsg) msg);
processor.processServerSideRpcTimeout((DeviceActorServerSideRpcTimeoutMsg) msg);
break;
case SESSION_TIMEOUT_MSG:
processor.checkSessionsTimeout();
@ -86,7 +86,7 @@ public class DeviceActor extends ContextAwareActor {
processor.processEdgeUpdate((DeviceEdgeUpdateMsg) msg);
break;
case REMOVE_RPC_TO_DEVICE_ACTOR_MSG:
processor.processRemoveRpc(ctx, (RemoveRpcActorMsg) msg);
processor.processRemoveRpc((RemoveRpcActorMsg) msg);
break;
default:
return false;

220
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -84,7 +84,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProt
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseStatusMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
@ -183,13 +182,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) {
ToDeviceRpcRequest request = msg.getMsg();
UUID rpcId = request.getId();
log.debug("[{}][{}] Received RPC request to process ...", deviceId, rpcId);
ToDeviceRpcRequestMsg rpcRequest = creteToDeviceRpcRequestMsg(request);
long timeout = request.getExpirationTime() - System.currentTimeMillis();
boolean persisted = request.isPersisted();
if (timeout <= 0) {
log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, request.getId(), request.getExpirationTime());
log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, rpcId, request.getExpirationTime());
if (persisted) {
createRpc(request, RpcStatus.EXPIRED);
}
@ -199,21 +200,23 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
boolean sent = false;
int requestId = rpcRequest.getRequestId();
if (systemContext.isEdgesEnabled() && edgeId != null) {
log.debug("[{}][{}] device is related to edge [{}]. Saving RPC request to edge queue", tenantId, deviceId, edgeId.getId());
log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId);
try {
saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()).get();
saveRpcRequestToEdgeQueue(request, requestId).get();
sent = true;
} catch (InterruptedException | ExecutionException e) {
log.error("[{}][{}][{}] Failed to save rpc request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e);
}
} else if (isSendNewRpcAvailable()) {
sent = rpcSubscriptions.size() > 0;
Set<UUID> syncSessionSet = new HashSet<>();
rpcSubscriptions.forEach((key, value) -> {
sendToTransport(rpcRequest, key, value.getNodeId());
if (SessionType.SYNC == value.getType()) {
syncSessionSet.add(key);
rpcSubscriptions.forEach((sessionId, sessionInfo) -> {
log.debug("[{}][{}][{}][{}] send RPC request to transport ...", deviceId, sessionId, rpcId, requestId);
sendToTransport(rpcRequest, sessionId, sessionInfo.getNodeId());
if (SessionType.SYNC == sessionInfo.getType()) {
syncSessionSet.add(sessionId);
}
});
log.trace("Rpc syncSessionSet [{}] subscription after sent [{}]", syncSessionSet, rpcSubscriptions);
@ -222,20 +225,20 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
if (persisted) {
ObjectNode response = JacksonUtil.newObjectNode();
response.put("rpcId", request.getId().toString());
response.put("rpcId", rpcId.toString());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null));
}
if (!persisted && request.isOneway() && sent) {
log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
log.debug("[{}] RPC command response sent [{}][{}]!", deviceId, rpcId, requestId);
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
} else {
registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
}
if (sent) {
log.debug("[{}] RPC request {} is sent!", deviceId, request.getId());
log.debug("[{}][{}][{}] RPC request is sent!", deviceId, rpcId, requestId);
} else {
log.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId());
log.debug("[{}][{}][{}] RPC request is NOT sent!", deviceId, rpcId, requestId);
}
}
@ -243,7 +246,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return !rpcSequential || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
}
private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) {
private void createRpc(ToDeviceRpcRequest request, RpcStatus status) {
Rpc rpc = new Rpc(new RpcId(request.getId()));
rpc.setCreatedTime(System.currentTimeMillis());
rpc.setTenantId(tenantId);
@ -252,7 +255,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
rpc.setRequest(JacksonUtil.valueToTree(request));
rpc.setStatus(status);
rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo()));
return systemContext.getTbRpcService().save(tenantId, rpc);
systemContext.getTbRpcService().save(tenantId, rpc);
}
private ToDeviceRpcRequestMsg creteToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
@ -269,82 +272,92 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
.build();
}
void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) {
log.debug("[{}] Processing rpc command response from edge session", deviceId);
void processRpcResponsesFromEdge(FromDeviceRpcResponseActorMsg responseMsg) {
log.debug("[{}] Processing RPC command response from edge session", deviceId);
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
boolean success = requestMd != null;
if (success) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(responseMsg.getMsg());
} else {
log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
log.debug("[{}] RPC command response [{}] is stale!", deviceId, responseMsg.getRequestId());
}
}
void processRemoveRpc(TbActorCtx context, RemoveRpcActorMsg msg) {
log.debug("[{}] Processing remove rpc command", msg.getRequestId());
void processRemoveRpc(RemoveRpcActorMsg msg) {
UUID requestId = msg.getRequestId();
log.debug("[{}][{}] Received remove RPC request ...", deviceId, requestId);
Map.Entry<Integer, ToDeviceRpcRequestMetadata> entry = null;
for (Map.Entry<Integer, ToDeviceRpcRequestMetadata> e : toDeviceRpcPendingMap.entrySet()) {
if (e.getValue().getMsg().getMsg().getId().equals(msg.getRequestId())) {
if (e.getValue().getMsg().getMsg().getId().equals(requestId)) {
entry = e;
break;
}
}
if (entry != null) {
Integer key = entry.getKey();
if (entry.getValue().isDelivered()) {
toDeviceRpcPendingMap.remove(entry.getKey());
toDeviceRpcPendingMap.remove(key);
} else {
Optional<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> firstRpc = getFirstRpc();
if (firstRpc.isPresent() && entry.getKey().equals(firstRpc.get().getKey())) {
toDeviceRpcPendingMap.remove(entry.getKey());
sendNextPendingRequest(context);
if (firstRpc.isPresent() && key.equals(firstRpc.get().getKey())) {
toDeviceRpcPendingMap.remove(key);
log.debug("[{}][{}][{}] Removed pending RPC! Going to send next pending request ...", deviceId, requestId, key);
sendNextPendingRequest();
} else {
toDeviceRpcPendingMap.remove(entry.getKey());
toDeviceRpcPendingMap.remove(key);
}
}
}
}
private void registerPendingRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
toDeviceRpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(rpcRequest.getRequestId(), timeout);
int requestId = rpcRequest.getRequestId();
UUID rpcId = new UUID(rpcRequest.getRequestIdMSB(), rpcRequest.getRequestIdLSB());
log.debug("[{}][{}][{}] Registering pending RPC request...", deviceId, rpcId, requestId);
toDeviceRpcPendingMap.put(requestId, new ToDeviceRpcRequestMetadata(msg, sent));
DeviceActorServerSideRpcTimeoutMsg timeoutMsg = new DeviceActorServerSideRpcTimeoutMsg(requestId, timeout);
scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
}
void processServerSideRpcTimeout(TbActorCtx context, DeviceActorServerSideRpcTimeoutMsg msg) {
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
void processServerSideRpcTimeout(DeviceActorServerSideRpcTimeoutMsg msg) {
Integer requestId = msg.getId();
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId);
if (requestMd != null) {
log.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
if (requestMd.getMsg().getMsg().isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.EXPIRED, null);
ToDeviceRpcRequest toDeviceRpcRequest = requestMd.getMsg().getMsg();
UUID rpcId = toDeviceRpcRequest.getId();
log.debug("[{}][{}][{}] RPC request timeout detected!", deviceId, rpcId, requestId);
if (toDeviceRpcRequest.isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), RpcStatus.EXPIRED, null);
}
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId,
null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
if (!requestMd.isDelivered()) {
sendNextPendingRequest(context);
log.debug("[{}][{}][{}] Pending RPC timeout detected! Going to send next pending request ...", deviceId, rpcId, requestId);
sendNextPendingRequest();
}
}
}
private void sendPendingRequests(TbActorCtx context, UUID sessionId, String nodeId) {
private void sendPendingRequests(UUID sessionId, String nodeId) {
SessionType sessionType = getSessionType(sessionId);
if (!toDeviceRpcPendingMap.isEmpty()) {
log.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, toDeviceRpcPendingMap.size(), sessionId);
log.debug("[{}] Pushing {} pending RPC messages to session: [{}]", deviceId, sessionId, toDeviceRpcPendingMap.size());
if (sessionType == SessionType.SYNC) {
log.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
log.debug("[{}] Cleanup sync RPC session [{}]", deviceId, sessionId);
rpcSubscriptions.remove(sessionId);
}
} else {
log.debug("[{}] No pending RPC messages for new async session [{}]", deviceId, sessionId);
log.debug("[{}] No pending RPC messages for session: [{}]", deviceId, sessionId);
}
Set<Integer> sentOneWayIds = new HashSet<>();
if (rpcSequential) {
getFirstRpc().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
getFirstRpc().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds));
} else if (sessionType == SessionType.ASYNC) {
toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(sessionId, nodeId, sentOneWayIds));
} else {
toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(sessionId, nodeId, sentOneWayIds));
}
sentOneWayIds.stream().filter(id -> !toDeviceRpcPendingMap.get(id).getMsg().getMsg().isPersisted()).forEach(toDeviceRpcPendingMap::remove);
@ -354,35 +367,38 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return toDeviceRpcPendingMap.entrySet().stream().filter(e -> !e.getValue().isDelivered()).findFirst();
}
private void sendNextPendingRequest(TbActorCtx context) {
private void sendNextPendingRequest() {
if (rpcSequential) {
rpcSubscriptions.forEach((id, s) -> sendPendingRequests(context, id, s.getNodeId()));
rpcSubscriptions.forEach((id, s) -> sendPendingRequests(id, s.getNodeId()));
}
}
private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(TbActorCtx context, UUID sessionId, String nodeId, Set<Integer> sentOneWayIds) {
private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(UUID sessionId, String nodeId, Set<Integer> sentOneWayIds) {
return entry -> {
ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
ToDeviceRpcRequestBody body = request.getBody();
Integer requestId = entry.getKey();
UUID rpcId = request.getId();
if (request.isOneway() && !rpcSequential) {
sentOneWayIds.add(entry.getKey());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null));
sentOneWayIds.add(requestId);
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
}
ToDeviceRpcRequestMsg rpcRequest = ToDeviceRpcRequestMsg.newBuilder()
.setRequestId(entry.getKey())
.setRequestId(requestId)
.setMethodName(body.getMethod())
.setParams(body.getParams())
.setExpirationTime(request.getExpirationTime())
.setRequestIdMSB(request.getId().getMostSignificantBits())
.setRequestIdLSB(request.getId().getLeastSignificantBits())
.setRequestIdMSB(rpcId.getMostSignificantBits())
.setRequestIdLSB(rpcId.getLeastSignificantBits())
.setOneway(request.isOneway())
.setPersisted(request.isPersisted())
.build();
log.debug("[{}][{}][{}][{}] Send pending RPC request to transport ...", deviceId, sessionId, rpcId, requestId);
sendToTransport(rpcRequest, sessionId, nodeId);
};
}
void process(TbActorCtx context, TransportToDeviceActorMsgWrapper wrapper) {
void process(TransportToDeviceActorMsgWrapper wrapper) {
TransportToDeviceActorMsg msg = wrapper.getMsg();
TbCallback callback = wrapper.getCallback();
var sessionInfo = msg.getSessionInfo();
@ -391,36 +407,36 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
processSessionStateMsgs(sessionInfo, msg.getSessionEvent());
}
if (msg.hasSubscribeToAttributes()) {
processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToAttributes());
processSubscriptionCommands(sessionInfo, msg.getSubscribeToAttributes());
}
if (msg.hasSubscribeToRPC()) {
processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToRPC());
processSubscriptionCommands(sessionInfo, msg.getSubscribeToRPC());
}
if (msg.hasSendPendingRPC()) {
sendPendingRequests(context, getSessionId(sessionInfo), sessionInfo.getNodeId());
sendPendingRequests(getSessionId(sessionInfo), sessionInfo.getNodeId());
}
if (msg.hasGetAttributes()) {
handleGetAttributesRequest(context, sessionInfo, msg.getGetAttributes());
handleGetAttributesRequest(sessionInfo, msg.getGetAttributes());
}
if (msg.hasToDeviceRPCCallResponse()) {
processRpcResponses(context, sessionInfo, msg.getToDeviceRPCCallResponse());
processRpcResponses(sessionInfo, msg.getToDeviceRPCCallResponse());
}
if (msg.hasSubscriptionInfo()) {
handleSessionActivity(context, sessionInfo, msg.getSubscriptionInfo());
handleSessionActivity(sessionInfo, msg.getSubscriptionInfo());
}
if (msg.hasClaimDevice()) {
handleClaimDeviceMsg(context, sessionInfo, msg.getClaimDevice());
handleClaimDeviceMsg(msg.getClaimDevice());
}
if (msg.hasRpcResponseStatusMsg()) {
processRpcResponseStatus(context, sessionInfo, msg.getRpcResponseStatusMsg());
processRpcResponseStatus(sessionInfo, msg.getRpcResponseStatusMsg());
}
if (msg.hasUplinkNotificationMsg()) {
processUplinkNotificationMsg(context, sessionInfo, msg.getUplinkNotificationMsg());
processUplinkNotificationMsg(sessionInfo, msg.getUplinkNotificationMsg());
}
callback.onSuccess();
}
private void processUplinkNotificationMsg(TbActorCtx context, SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) {
private void processUplinkNotificationMsg(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg uplinkNotificationMsg) {
String nodeId = sessionInfo.getNodeId();
sessions.entrySet().stream()
.filter(kv -> kv.getValue().getSessionInfo().getNodeId().equals(nodeId) && (kv.getValue().isSubscribedToAttributes() || kv.getValue().isSubscribedToRPC()))
@ -434,7 +450,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
});
}
private void handleClaimDeviceMsg(TbActorCtx context, SessionInfoProto sessionInfo, ClaimDeviceMsg msg) {
private void handleClaimDeviceMsg(ClaimDeviceMsg msg) {
DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB()));
systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs());
}
@ -447,7 +463,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId);
}
private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
private void handleGetAttributesRequest(SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
int requestId = request.getRequestId();
if (request.getOnlyShared()) {
Futures.addCallback(findAllAttributesByScope(DataConstants.SHARED_SCOPE), new FutureCallback<>() {
@ -531,7 +547,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return sessions.containsKey(sessionId) ? SessionType.ASYNC : SessionType.SYNC;
}
void processAttributesUpdate(TbActorCtx context, DeviceAttributesEventNotificationMsg msg) {
void processAttributesUpdate(DeviceAttributesEventNotificationMsg msg) {
if (attributeSubscriptions.size() > 0) {
boolean hasNotificationData = false;
AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
@ -568,19 +584,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
private void processRpcResponses(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) {
private void processRpcResponses(SessionInfoProto sessionInfo, ToDeviceRpcResponseMsg responseMsg) {
UUID sessionId = getSessionId(sessionInfo);
log.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
log.debug("[{}][{}] Processing RPC command response: {}", deviceId, sessionId, responseMsg);
int requestId = responseMsg.getRequestId();
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(requestId);
boolean success = requestMd != null;
if (success) {
ToDeviceRpcRequest toDeviceRequestMsg = requestMd.getMsg().getMsg();
boolean delivered = requestMd.isDelivered();
boolean hasError = StringUtils.isNotEmpty(responseMsg.getError());
try {
String payload = hasError ? responseMsg.getError() : responseMsg.getPayload();
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(
new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
payload, null));
if (requestMd.getMsg().getMsg().isPersisted()) {
new FromDeviceRpcResponse(toDeviceRequestMsg.getId(), payload, null));
if (toDeviceRequestMsg.isPersisted()) {
RpcStatus status = hasError ? RpcStatus.FAILED : RpcStatus.SUCCESSFUL;
JsonNode response;
try {
@ -588,28 +606,33 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
} catch (IllegalArgumentException e) {
response = JacksonUtil.newObjectNode().put("error", payload);
}
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), status, response);
systemContext.getTbRpcService().save(tenantId, new RpcId(toDeviceRequestMsg.getId()), status, response);
}
} finally {
if (hasError && !requestMd.isDelivered()) {
sendNextPendingRequest(context);
if (!delivered) {
String errorResponse = hasError ? "error" : "";
log.debug("[{}][{}][{}] Received {} response for undelivered RPC! Going to send next pending request ...", deviceId, sessionId, requestId, errorResponse);
sendNextPendingRequest();
}
}
} else {
log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
log.debug("[{}][{}][{}] RPC command response is stale!", deviceId, sessionId, requestId);
}
}
private void processRpcResponseStatus(TbActorCtx context, SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) {
private void processRpcResponseStatus(SessionInfoProto sessionInfo, ToDeviceRpcResponseStatusMsg responseMsg) {
UUID rpcId = new UUID(responseMsg.getRequestIdMSB(), responseMsg.getRequestIdLSB());
RpcStatus status = RpcStatus.valueOf(responseMsg.getStatus());
ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(responseMsg.getRequestId());
UUID sessionId = getSessionId(sessionInfo);
int requestId = responseMsg.getRequestId();
log.debug("[{}][{}][{}][{}] Processing RPC command response status: [{}]", deviceId, sessionId, rpcId, requestId, status);
ToDeviceRpcRequestMetadata md = toDeviceRpcPendingMap.get(requestId);
if (md != null) {
JsonNode response = null;
if (status.equals(RpcStatus.DELIVERED)) {
if (md.getMsg().getMsg().isOneway()) {
toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
toDeviceRpcPendingMap.remove(requestId);
if (rpcSequential) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
}
@ -620,7 +643,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
Integer maxRpcRetries = md.getMsg().getMsg().getRetries();
maxRpcRetries = maxRpcRetries == null ? systemContext.getMaxRpcRetries() : Math.min(maxRpcRetries, systemContext.getMaxRpcRetries());
if (maxRpcRetries <= md.getRetries()) {
toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
toDeviceRpcPendingMap.remove(requestId);
status = RpcStatus.FAILED;
response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
} else {
@ -632,17 +655,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), status, response);
}
if (status != RpcStatus.SENT) {
sendNextPendingRequest(context);
log.debug("[{}][{}][{}][{}] RPC was {}! Going to send next pending request ...", deviceId, sessionId, rpcId, requestId, status.name().toLowerCase());
sendNextPendingRequest();
}
} else {
log.info("[{}][{}] Rpc has already removed from pending map.", deviceId, rpcId);
log.warn("[{}][{}][{}][{}] RPC has already been removed from pending map.", deviceId, sessionId, rpcId, requestId);
}
}
private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
UUID sessionId = getSessionId(sessionInfo);
if (subscribeCmd.getUnsubscribe()) {
log.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
log.debug("[{}] Canceling attributes subscription for session: [{}]", deviceId, sessionId);
attributeSubscriptions.remove(sessionId);
} else {
SessionInfoMetaData sessionMD = sessions.get(sessionId);
@ -650,7 +674,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId()));
}
sessionMD.setSubscribedToAttributes(true);
log.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
log.debug("[{}] Registering attributes subscription for session: [{}]", deviceId, sessionId);
attributeSubscriptions.put(sessionId, sessionMD.getSessionInfo());
dumpSessions();
}
@ -660,10 +684,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
return new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
}
private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) {
private void processSubscriptionCommands(SessionInfoProto sessionInfo, SubscribeToRPCMsg subscribeCmd) {
UUID sessionId = getSessionId(sessionInfo);
if (subscribeCmd.getUnsubscribe()) {
log.debug("[{}] Canceling rpc subscription for session [{}]", deviceId, sessionId);
log.debug("[{}] Canceling RPC subscription for session: [{}]", deviceId, sessionId);
rpcSubscriptions.remove(sessionId);
} else {
SessionInfoMetaData sessionMD = sessions.get(sessionId);
@ -671,9 +695,9 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
sessionMD = new SessionInfoMetaData(new SessionInfo(subscribeCmd.getSessionType(), sessionInfo.getNodeId()));
}
sessionMD.setSubscribedToRPC(true);
log.debug("[{}] Registering rpc subscription for session [{}]", deviceId, sessionId);
rpcSubscriptions.put(sessionId, sessionMD.getSessionInfo());
sendPendingRequests(context, sessionId, sessionInfo.getNodeId());
log.debug("[{}] Registered RPC subscription for session: [{}] Going to check for pending requests ...", deviceId, sessionId);
sendPendingRequests(sessionId, sessionInfo.getNodeId());
dumpSessions();
}
}
@ -683,10 +707,10 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
Objects.requireNonNull(sessionId);
if (msg.getEvent() == SessionEvent.OPEN) {
if (sessions.containsKey(sessionId)) {
log.debug("[{}] Received duplicate session open event [{}]", deviceId, sessionId);
log.debug("[{}][{}] Received duplicate session open event.", deviceId, sessionId);
return;
}
log.debug("[{}] Processing new session [{}]. Current sessions size {}", deviceId, sessionId, sessions.size());
log.debug("[{}] Processing new session: [{}] Current sessions size: {}", deviceId, sessionId, sessions.size());
sessions.put(sessionId, new SessionInfoMetaData(new SessionInfo(SessionType.ASYNC, sessionInfo.getNodeId())));
if (sessions.size() == 1) {
@ -695,7 +719,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis());
dumpSessions();
} else if (msg.getEvent() == SessionEvent.CLOSED) {
log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
log.debug("[{}][{}] Canceling subscriptions for closed session.", deviceId, sessionId);
sessions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
rpcSubscriptions.remove(sessionId);
@ -706,7 +730,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
private void handleSessionActivity(TbActorCtx context, SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) {
private void handleSessionActivity(SessionInfoProto sessionInfoProto, SubscriptionInfoProto subscriptionInfo) {
UUID sessionId = getSessionId(sessionInfoProto);
Objects.requireNonNull(sessionId);
@ -743,7 +767,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) {
log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId [{}] sessionMd [{}]", sessionId, sessionMd);
log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId: [{}] sessionMd: [{}]", sessionId, sessionMd);
notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!");
}
@ -807,14 +831,6 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getTbCoreToTransportService().process(nodeId, msg);
}
private void sendToTransport(ToServerRpcResponseMsg rpcMsg, UUID sessionId, String nodeId) {
ToTransportMsg msg = ToTransportMsg.newBuilder()
.setSessionIdMSB(sessionId.getMostSignificantBits())
.setSessionIdLSB(sessionId.getLeastSignificantBits())
.setToServerResponse(rpcMsg).build();
systemContext.getTbCoreToTransportService().process(nodeId, msg);
}
private ListenableFuture<Void> saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) {
ObjectNode body = JacksonUtil.newObjectNode();
body.put("requestId", requestId);
@ -897,14 +913,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
log.debug("[{}] Restored session: {}", deviceId, sessionMD);
}
log.debug("[{}] Restored sessions: {}, rpc subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
log.debug("[{}] Restored sessions: {}, RPC subscriptions: {}, attribute subscriptions: {}", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
}
private void dumpSessions() {
if (systemContext.isLocalCacheType()) {
return;
}
log.debug("[{}] Dumping sessions: {}, rpc subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
log.debug("[{}] Dumping sessions: {}, RPC subscriptions: {}, attribute subscriptions: {} to cache", deviceId, sessions.size(), rpcSubscriptions.size(), attributeSubscriptions.size());
List<SessionSubscriptionInfoProto> sessionsList = new ArrayList<>(sessions.size());
sessions.forEach((uuid, sessionMD) -> {
if (sessionMD.getSessionInfo().getType() == SessionType.SYNC) {

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

@ -34,6 +34,7 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -214,6 +215,12 @@ class DefaultTbContext implements TbContext {
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null);
}
@Override
public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), getFailureMessage(th), null, null);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType) {
TopicPartitionInfo tpi = resolvePartition(tbMsg);
@ -311,16 +318,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
}
String failureMessage;
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
}
String failureMessage = getFailureMessage(th);
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(),
nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE),
msg, failureMessage));
@ -724,6 +722,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSlackService();
}
@Override
public boolean isExternalNodeForceAck() {
return mainCtx.isExternalNodeForceAck();
}
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();
@ -840,10 +843,24 @@ class DefaultTbContext implements TbContext {
}
@Override
public void checkTenantEntity(EntityId entityId) {
public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
throw new RuntimeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.");
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
}
}
private static String getFailureMessage(Throwable th) {
String failureMessage;
if (th != null) {
if (!StringUtils.isEmpty(th.getMessage())) {
failureMessage = th.getMessage();
} else {
failureMessage = th.getClass().getSimpleName();
}
} else {
failureMessage = null;
}
return failureMessage;
}
private class SimpleTbQueueCallback implements TbQueueCallback {

9
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java

@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.RuleChainErrorActor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.dao.rule.RuleChainService;
import java.util.function.Function;
@ -86,7 +88,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
() -> DefaultActorService.RULE_DISPATCHER_NAME,
() -> {
RuleChain ruleChain = provider.apply(ruleChainId);
return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);
if (ruleChain == null) {
return new RuleChainErrorActor.ActorCreator(systemContext, tenantId,
new RuleEngineException("Rule Chain with id: " + ruleChainId + " not found!"));
} else {
return new RuleChainActor.ActorCreator(systemContext, tenantId, ruleChain);
}
});
}

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger;
public abstract class RuleEngineComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ComponentActor<T, P> {

78
application/src/main/java/org/thingsboard/server/actors/shared/RuleChainErrorActor.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.shared;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbStringActorId;
import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import java.util.UUID;
@Slf4j
public class RuleChainErrorActor extends ContextAwareActor {
private final TenantId tenantId;
private final RuleEngineException error;
private RuleChainErrorActor(ActorSystemContext systemContext, TenantId tenantId, RuleEngineException error) {
super(systemContext);
this.tenantId = tenantId;
this.error = error;
}
@Override
protected boolean doProcess(TbActorMsg msg) {
if (msg instanceof RuleChainAwareMsg) {
log.debug("[{}] Reply with {} for message {}", tenantId, error.getMessage(), msg);
var rcMsg = (RuleChainAwareMsg) msg;
rcMsg.getMsg().getCallback().onFailure(error);
return true;
} else {
return false;
}
}
public static class ActorCreator extends ContextBasedCreator {
private final TenantId tenantId;
private final RuleEngineException error;
public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleEngineException error) {
super(context);
this.tenantId = tenantId;
this.error = error;
}
@Override
public TbActorId createActorId() {
return new TbStringActorId(UUID.randomUUID().toString());
}
@Override
public TbActor createActor() {
return new RuleChainErrorActor(context, tenantId, error);
}
}
}

5
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -49,7 +49,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -211,10 +210,6 @@ public class TenantActor extends RuleChainManagerActor {
log.trace("[{}] Ack message because Rule Engine is disabled", tenantId);
tbMsg.getCallback().onSuccess();
}
systemContext.getNotificationRuleProcessor().process(RuleEngineMsgTrigger.builder()
.tenantId(tenantId)
.msg(tbMsg)
.build());
}
private void onRuleChainMsg(RuleChainAwareMsg msg) {

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

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -49,7 +49,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
try {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId())) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_TENANT, user.getTenantId())) {
rateLimitExceeded(EntityType.TENANT, response);
return;
}
@ -60,7 +60,7 @@ public class RateLimitProcessingFilter extends OncePerRequestFilter {
}
if (user.isCustomerUser()) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS, user.getTenantId(), user.getCustomerId())) {
if (!rateLimitService.checkRateLimit(LimitedApi.REST_REQUESTS_PER_CUSTOMER, user.getTenantId(), user.getCustomerId())) {
rateLimitExceeded(EntityType.CUSTOMER, response);
return;
}

9
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -78,6 +78,7 @@ public class ThingsboardSecurityConfiguration {
protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[]{"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**", "/api/license/**"};
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
@Autowired
private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@ -142,7 +143,7 @@ public class ThingsboardSecurityConfiguration {
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List<String> pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT));
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@ -195,7 +196,7 @@ public class ThingsboardSecurityConfiguration {
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.cacheControl(config -> {})
.frameOptions(config -> {}))
.frameOptions(config -> {}).disable())
.cors(cors -> {})
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(config -> {})
@ -206,7 +207,9 @@ public class ThingsboardSecurityConfiguration {
.requestMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
.requestMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
.requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
.requestMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll()) // static resources, user activation and password reset end-points
.authorizeRequests(config -> config
.requestMatchers(WS_TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() // Protected WebSocket API End-points
.requestMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()) // Protected API End-points
.exceptionHandling(config -> config.accessDeniedHandler(restAccessDeniedHandler))

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

@ -15,14 +15,27 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -33,18 +46,24 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
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.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
@ -59,6 +78,7 @@ import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
@ -69,15 +89,25 @@ import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsSer
import org.thingsboard.server.service.system.SystemInfoService;
import org.thingsboard.server.service.update.UpdateService;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@Slf4j
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController extends BaseController {
private static final String PREV_URI_PATH_PARAMETER = "prevUri";
private static final String PREV_URI_COOKIE_NAME = "prev_uri";
private static final String STATE_COOKIE_NAME = "state";
private static final String MAIL_SETTINGS_KEY = "mail";
private final MailService mailService;
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
@ -104,6 +134,7 @@ public class AdminController extends BaseController {
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key);
if (adminSettings.getKey().equals("mail")) {
((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
}
return adminSettings;
}
@ -124,6 +155,7 @@ public class AdminController extends BaseController {
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
} else if (adminSettings.getKey().equals("sms")) {
smsService.updateSmsConfiguration();
}
@ -190,9 +222,19 @@ public class AdminController extends BaseController {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) {
if (!adminSettings.getJsonValue().has("password")) {
if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken");
if (refreshToken == null) {
throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL);
}
ObjectNode settings = (ObjectNode) adminSettings.getJsonValue();
settings.put("refreshToken", refreshToken.asText());
} else {
if (!adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
}
}
String email = getCurrentUser().getEmail();
mailService.sendTestMail(adminSettings.getJsonValue(), email);
@ -364,4 +406,84 @@ public class AdminController extends BaseController {
return systemInfoService.getFeaturesInfo();
}
@ApiOperation(value = "Get OAuth2 log in processing URL (getMailProcessingUrl)", notes = "Returns the URL enclosed in " +
"double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " +
"further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET)
@ResponseBody
public String getMailProcessingUrl() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return "\"/api/admin/mail/oauth2/code\"";
}
@ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" +
"provider sends authorization code to specified redirect uri.)")
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text")
public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
String state = StringUtils.generateSafeToken();
if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180);
}
CookieUtils.addCookie(response, STATE_COOKIE_NAME, state, 180);
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found");
JsonNode jsonValue = adminSettings.getJsonValue();
String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText();
String authUri = checkNotNull(jsonValue.get("authUri"), "No authorization uri was configured").asText();
String redirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText();
List<String> scope = JacksonUtil.convertValue(checkNotNull(jsonValue.get("scope"), "No scope was configured"), new TypeReference<>() {
});
return "\"" + new AuthorizationCodeRequestUrl(authUri, clientId)
.setScopes(scope)
.setState(state)
.setRedirectUri(redirectUri)
.build() + "\"";
}
@RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET)
public void codeProcessingUrl(
@RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {
Optional<Cookie> prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME);
Optional<Cookie> cookieState = CookieUtils.getCookie(request, STATE_COOKIE_NAME);
String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
String prevUri = baseUrl + (prevUrlOpt.isPresent() ? prevUrlOpt.get().getValue() : "/settings/outgoing-mail");
if (cookieState.isEmpty() || !cookieState.get().getValue().equals(state)) {
CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME);
throw new ThingsboardException("Refresh token was not generated, invalid state param", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
CookieUtils.deleteCookie(request, response, STATE_COOKIE_NAME);
CookieUtils.deleteCookie(request, response, PREV_URI_COOKIE_NAME);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY), "No Administration mail settings found");
JsonNode jsonValue = adminSettings.getJsonValue();
String clientId = checkNotNull(jsonValue.get("clientId"), "No clientId was configured").asText();
String clientSecret = checkNotNull(jsonValue.get("clientSecret"), "No client secret was configured").asText();
String clientRedirectUri = checkNotNull(jsonValue.get("redirectUri"), "No Redirect uri was configured").asText();
String tokenUri = checkNotNull(jsonValue.get("tokenUri"), "No authorization uri was configured").asText();
TokenResponse tokenResponse;
try {
tokenResponse = new AuthorizationCodeTokenRequest(new NetHttpTransport(), new GsonFactory(), new GenericUrl(tokenUri), code)
.setRedirectUri(clientRedirectUri)
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute();
} catch (IOException e) {
log.warn("Unable to retrieve refresh token: {}", e.getMessage());
throw new ThingsboardException("Error while requesting access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL);
}
((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode) jsonValue).put("tokenGenerated", true);
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings);
response.sendRedirect(prevUri);
}
}

2
application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java

@ -78,7 +78,7 @@ public class AlarmCommentController extends BaseController {
@PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}

2
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
@ -49,7 +50,6 @@ import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;

162
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -110,6 +110,8 @@ public class ControllerConstants {
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
protected static final String RESOURCE_TYPE = "A string value representing the resource type.";
protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. ";
protected static final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name.";
@ -173,45 +175,130 @@ public class ControllerConstants {
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION =
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" +
" },\n" +
" \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" +
" \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"id\": \"null\",\n" +
" \"createdTime\": 0,\n" +
" \"deviceId\": \"null\",\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\": {\n" +
" \"client\": {\n" +
" \"endpoint\": \"LwRpk00000000\",\n" +
" \"securityConfigClientMode\": \"RPK\",\n" +
" \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
" },\n" +
" \"bootstrap\": {\n" +
" \"bootstrapServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" },\n" +
" \"lwm2mServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"label\":\"Label_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"type\": \"default\"\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une3\"\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String certificateValue = "\"-----BEGIN CERTIFICATE----- " +
"MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1owbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qKQiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaCkQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8GA1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZuccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== " +
"-----END CERTIFICATE-----\"";
protected static final String certificateId = "\"84f5911765abba1f96bf4165604e9e90338fc6214081a8e623b6ff9669aedb27\"";
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_X509_Certificate\",\n" +
" \"label\":\"Label_DeviceWithCredantial_X509_Certificate\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"X509_CERTIFICATE\",\n" +
" \"credentialsId\": " + certificateId + ",\n" +
" \"credentialsValue\": " + certificateValue + "\n" +
" }\n" +
"}";
protected static final String MQTT_BASIC_VALUE = "\"{\\\"clientId\\\":\\\"5euh5nzm34bjjh1efmlt\\\",\\\"userName\\\":\\\"onasd1lgwasmjl7v2v7h\\\",\\\"password\\\":\\\"b9xtm4ny8kt9zewaga5o\\\"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"label\":\"Label_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"MQTT_BASIC\",\n" +
" \"credentialsValue\": " + MQTT_BASIC_VALUE + "\n" +
" }\n" +
"}";
protected static final String CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION =
" \"{" +
"\\\"client\\\":{ " +
"\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
"\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
"\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\"" +
" }, " +
"\\\"bootstrap\\\":{ " +
"\\\"bootstrapServer\\\":{ " +
"\\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}, " +
"\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}" +
"} " +
"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_LwRpk00000000\",\n" +
" \"label\":\"Label_LwRpk00000000\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"a660bd50-10ef-11ee-8737-b5634e73c779\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\":\n" + CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION + "\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
"Provides a hint about the data type of the entity field that is defined in the filter key. " +
"The value type impacts the list of possible operations that you may use in the corresponding predicate. For example, you may use 'STARTS_WITH' or 'END_WITH', but you can't use 'GREATER_OR_EQUAL' for string values." +
"The following filter value types and corresponding predicate operations are supported: " + NEW_LINE +
@ -220,7 +307,7 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
@ -235,7 +322,7 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
@ -287,9 +374,9 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
@ -305,7 +392,8 @@ public class ControllerConstants {
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +

23
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -95,7 +95,11 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_NAME_
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
@ -183,9 +187,20 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
"Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " +
"You may find the example of LwM2M device and RPK credentials below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN +
"Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
"You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile default</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")

2
application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java

@ -86,6 +86,6 @@ public class EdgeEventController extends BaseController {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false));
return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink));
}
}

56
application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.mail.TbMailConfigTemplateService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api/mail/config/template")
@Slf4j
public class MailConfigTemplateController extends BaseController {
private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports";
private final TbMailConfigTemplateService mailConfigTemplateService;
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
notes = MAIL_CONFIG_TEMPLATE_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return mailConfigTemplateService.findAllMailConfigTemplates();
}
}

2
application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java

@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;

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

@ -460,7 +460,7 @@ public class RuleChainController extends BaseController {
@Parameter(description = "Enables overwrite for existing rule chains with the same name.")
@RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite);
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration);
for (RuleChainImportResult importResult : importResults) {
if (importResult.getError() == null) {
tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(),

87
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -22,19 +22,25 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@ -50,6 +56,7 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.Base64;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -58,6 +65,7 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_DES
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
@ -71,6 +79,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RequiredArgsConstructor
public class TbResourceController extends BaseController {
private static final String DOWNLOAD_RESOURCE_IF_NOT_CHANGED = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed.";
private final TbResourceService tbResourceService;
public static final String RESOURCE_ID = "resourceId";
@ -94,6 +103,47 @@ public class TbResourceController extends BaseController {
.body(resource);
}
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET, produces = "application/xml")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadLwm2mResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag);
}
@ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-pkcs12")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadPkcs12ResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag);
}
@ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)",
notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-java-keystore")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJksResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag);
}
@ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET, produces = "application/javascript")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJsResourceIfChanged(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag);
}
@ApiOperation(value = "Get Resource Info (getResourceInfoById)",
notes = "Fetch the Resource Info object based on the provided Resource Id. " +
RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
@ -152,6 +202,8 @@ public class TbResourceController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = RESOURCE_TYPE, schema = @Schema(allowableValues = {"LWM2M_MODEL", "JKS", "PKCS_12", "JS_MODULE"}))
@RequestParam(required = false) String resourceType,
@Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "resourceType", "tenantId"}))
@ -159,10 +211,15 @@ public class TbResourceController extends BaseController {
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder();
filter.tenantId(getTenantId());
if (StringUtils.isNotEmpty(resourceType)) {
filter.resourceType(ResourceType.valueOf(resourceType));
}
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink));
return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink));
} else {
return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink));
return checkNotNull(resourceService.findAllTenantResourcesByTenantId(filter.build(), pageLink));
}
}
@ -215,4 +272,30 @@ public class TbResourceController extends BaseController {
TbResource tbResource = checkResourceId(resourceId, Operation.DELETE);
tbResourceService.delete(tbResource, getCurrentUser());
}
private ResponseEntity<org.springframework.core.io.Resource> downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
if (etag.equals(tbResourceInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getEtag())
.build();
}
}
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes()));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
.header("x-filename", tbResource.getFileName())
.contentLength(resource.contentLength())
.header("Content-Type", type.getMediaType())
.cacheControl(CacheControl.noCache())
.eTag(tbResource.getEtag())
.body(resource);
}
}

2
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;

3
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -260,6 +260,8 @@ public class ThingsboardInstallService {
case "3.5.1":
log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.5.1");
dataUpdateService.updateData("3.5.1");
systemDataLoaderService.updateDefaultNotificationConfigs();
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:
@ -268,6 +270,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.updateSystemWidgets();
installScripts.loadSystemLwm2mResources();
}

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

@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -40,10 +42,12 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import java.util.List;
import java.util.Map;
@ -241,19 +245,7 @@ public class EntityActionService {
}
}
if (tenantId != null && !tenantId.isSysTenantId()) {
if (actionType == ActionType.ADDED) {
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
}
notificationRuleProcessor.process(EntityActionTrigger.builder()
.tenantId(tenantId)
.entityId(entityId)
.entity(entity)
.actionType(actionType)
.user(user)
.build());
processNotificationRules(tenantId, entityId, entity, actionType, user, additionalInfo);
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
@ -263,6 +255,53 @@ public class EntityActionService {
}
}
private void processNotificationRules(TenantId tenantId, EntityId entityId, HasName entity, ActionType actionType, User user, Object... additionalInfo) {
switch (actionType) {
case ADDED:
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
case UPDATED:
case DELETED:
notificationRuleProcessor.process(EntityActionTrigger.builder()
.tenantId(tenantId)
.entityId(entityId)
.entity(entity)
.actionType(actionType)
.user(user)
.build());
break;
case ALARM_ASSIGNED:
case ALARM_UNASSIGNED:
if (!(entity instanceof AlarmInfo)) { // should not normally happen
log.warn("Invalid alarm assignment event: entity is not instance of AlarmInfo");
break;
}
notificationRuleProcessor.process(AlarmAssignmentTrigger.builder()
.tenantId(tenantId)
.alarmInfo((AlarmInfo) entity)
.actionType(actionType)
.user(user)
.build());
break;
case ADDED_COMMENT:
case UPDATED_COMMENT:
if (!(entity instanceof Alarm)) { // should not normally happen
log.warn("Invalid alarm comment event: entity is not instance of Alarm");
break;
}
notificationRuleProcessor.process(AlarmCommentTrigger.builder()
.tenantId(tenantId)
.comment(extractParameter(AlarmComment.class, 0, additionalInfo))
.alarm((Alarm) entity)
.actionType(actionType)
.user(user)
.build());
break;
}
}
public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {

4
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -61,7 +61,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceM
import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.BaseApiUsageState.StatsCalculationResult;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.mail.MailExecutorService;

82
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.NodeDefinition;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
@ -48,12 +49,14 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
public static final int MAX_OPTIMISITC_RETRIES = 3;
@Value("${plugins.scan_packages}")
private String[] scanPackages;
@ -63,11 +66,13 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Autowired
private ComponentDescriptorService componentDescriptorService;
private Map<String, ComponentDescriptor> components = new HashMap<>();
private final Map<String, RuleNodeClassInfo> ruleNodeClasses = new HashMap<>();
private final Map<String, ComponentDescriptor> components = new HashMap<>();
private Map<ComponentType, List<ComponentDescriptor>> coreComponentsMap = new HashMap<>();
private final Map<ComponentType, List<ComponentDescriptor>> coreComponentsMap = new HashMap<>();
private Map<ComponentType, List<ComponentDescriptor>> edgeComponentsMap = new HashMap<>();
private final Map<ComponentType, List<ComponentDescriptor>> edgeComponentsMap = new HashMap<>();
private boolean isInstall() {
return environment.acceptsProfiles(Profiles.of("install"));
@ -75,28 +80,62 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@PostConstruct
public void init() {
for (var def : discoverBeansByAnnotationType(RuleNode.class)) {
String clazzName = def.getBeanClassName();
try {
var clazz = Class.forName(clazzName);
RuleNode annotation = clazz.getAnnotation(RuleNode.class);
boolean versioned = false;
if (annotation.version() > 0) { // No need to process nodes that has version = 0;
if (TbVersionedNode.class.isAssignableFrom(clazz)) {
versioned = true;
} else {
log.error("RuleNode [{}] has version {} but does not implement TbVersionedNode interface! Any update procedures for this rule node will be skipped!", clazzName, annotation.version());
}
}
ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation, versioned));
} catch (Exception e) {
log.warn("Failed to create instance of rule node type: {} due to: ", clazzName, e);
}
}
if (!isInstall()) {
discoverComponents();
}
}
private Set<BeanDefinition> discoverBeansByAnnotationType(Class<? extends Annotation> annotationType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
Set<BeanDefinition> defs = new HashSet<>();
for (String scanPackage : scanPackages) {
defs.addAll(scanner.findCandidateComponents(scanPackage));
}
return defs;
}
@Override
public Optional<RuleNodeClassInfo> getRuleNodeInfo(String clazz) {
return Optional.ofNullable(ruleNodeClasses.get(clazz));
}
@Override
public List<RuleNodeClassInfo> getVersionedNodes() {
return ruleNodeClasses.values().stream().filter(RuleNodeClassInfo::isVersioned).collect(Collectors.toList());
}
private void registerRuleNodeComponents() {
Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
for (BeanDefinition def : ruleNodeBeanDefinitions) {
for (RuleNodeClassInfo def : ruleNodeClasses.values()) {
int retryCount = 0;
Exception cause = null;
while (retryCount < MAX_OPTIMISITC_RETRIES) {
try {
String clazzName = def.getBeanClassName();
Class<?> clazz = Class.forName(clazzName);
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
ComponentType type = ruleNodeAnnotation.type();
ComponentType type = def.getAnnotation().type();
ComponentDescriptor component = scanAndPersistComponent(def, type);
components.put(component.getClazz(), component);
putComponentIntoMaps(type, ruleNodeAnnotation, component);
putComponentIntoMaps(type, def.getAnnotation(), component);
break;
} catch (Exception e) {
log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
log.trace("Can't initialize component {}, due to {}", def.getClassName(), e.getMessage(), e);
cause = e;
retryCount++;
try {
@ -107,7 +146,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
}
if (cause != null && retryCount == MAX_OPTIMISITC_RETRIES) {
log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), cause.getMessage(), cause);
log.error("Can't initialize component {}, due to {}", def.getClassName(), cause.getMessage(), cause);
throw new RuntimeException(cause);
}
}
@ -144,13 +183,14 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return false;
}
private ComponentDescriptor scanAndPersistComponent(BeanDefinition def, ComponentType type) {
private ComponentDescriptor scanAndPersistComponent(RuleNodeClassInfo def, ComponentType type) {
ComponentDescriptor scannedComponent = new ComponentDescriptor();
String clazzName = def.getBeanClassName();
String clazzName = def.getClassName();
try {
scannedComponent.setType(type);
Class<?> clazz = Class.forName(clazzName);
Class<?> clazz = def.getClazz();
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setConfigurationVersion(def.isVersioned() ? def.getCurrentVersion() : 0);
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
@ -162,7 +202,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
scannedComponent.setClazz(clazzName);
log.debug("Processing scanned component: {}", scannedComponent);
} catch (Exception e) {
log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
log.error("Can't initialize component {}, due to {}", clazzName, e.getMessage(), e);
throw new RuntimeException(e);
}
ComponentDescriptor persistedComponent = componentDescriptorService.findByClazz(TenantId.SYS_TENANT_ID, clazzName);
@ -210,16 +250,6 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return relationTypes.toArray(new String[relationTypes.size()]);
}
private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
Set<BeanDefinition> defs = new HashSet<>();
for (String scanPackage : scanPackages) {
defs.addAll(scanner.findCandidateComponents(scanPackage));
}
return defs;
}
@Override
public void discoverComponents() {
registerRuleNodeComponents();

6
application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java

@ -19,7 +19,9 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@ -30,6 +32,10 @@ public interface ComponentDiscoveryService {
void discoverComponents();
Optional<RuleNodeClassInfo> getRuleNodeInfo(String clazz);
List<RuleNodeClassInfo> getVersionedNodes();
List<ComponentDescriptor> getComponents(ComponentType type, RuleChainType ruleChainType);
List<ComponentDescriptor> getComponents(Set<ComponentType> types, RuleChainType ruleChainType);

40
application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.component;
import lombok.Data;
import org.thingsboard.rule.engine.api.RuleNode;
@Data
public class RuleNodeClassInfo {
private final Class<?> clazz;
private final RuleNode annotation;
private final boolean versioned;
public String getClassName(){
return clazz.getName();
}
public String getSimpleName() {
return clazz.getSimpleName();
}
public int getCurrentVersion() {
return annotation.version();
}
}

38
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -341,7 +341,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
sessionNewEvents.put(edgeId, false);
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
}
scheduleEdgeEventsCheck(session);
}
@ -380,21 +383,26 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
private void onEdgeDisconnect(EdgeId edgeId) {
log.info("[{}] edge disconnected!", edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
sessionNewEvents.remove(edgeId);
} finally {
newEventLock.unlock();
private void onEdgeDisconnect(EdgeId edgeId, UUID sessionId) {
log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) {
toRemove = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
sessionNewEvents.remove(edgeId);
} finally {
newEventLock.unlock();
}
save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis();
save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
} else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
}
save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis();
save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
pushRuleEngineMessage(removed.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
}
private void save(EdgeId edgeId, String key, long value) {

245
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
@ -68,17 +71,15 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j
@Data
@ -89,10 +90,11 @@ public final class EdgeGrpcSession implements Closeable {
private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
private final Consumer<EdgeId> sessionCloseListener;
private final BiConsumer<EdgeId, UUID> sessionCloseListener;
private final EdgeSessionState sessionState = new EdgeSessionState();
@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable {
private boolean connected;
private boolean syncCompleted;
private Long newStartTs;
private Long previousStartTs;
private Long newStartSeqId;
private Long previousStartSeqId;
private Long seqIdEnd;
private EdgeVersion edgeVersion;
private int maxInboundMessageSize;
@ -111,7 +119,7 @@ public final class EdgeGrpcSession implements Closeable {
private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener,
Consumer<EdgeId> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
BiConsumer<EdgeId, UUID> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
this.sessionId = UUID.randomUUID();
this.ctx = ctx;
this.outputStream = outputStream;
@ -180,7 +188,7 @@ public final class EdgeGrpcSession implements Closeable {
connected = false;
if (edge != null) {
try {
sessionCloseListener.accept(edge.getId());
sessionCloseListener.accept(edge.getId(), sessionId);
} catch (Exception ignored) {
}
}
@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable {
EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(next);
Futures.addCallback(uuidListenableFuture, new FutureCallback<>() {
ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable UUID result) {
public void onSuccess(@Nullable Pair<Long, Long> result) {
doSync(cursor);
}
@ -288,7 +296,7 @@ public final class EdgeGrpcSession implements Closeable {
} catch (Exception e) {
log.error("[{}] Failed to send downlink message [{}]", this.sessionId, downlinkMsg, e);
connected = false;
sessionCloseListener.accept(edge.getId());
sessionCloseListener.accept(edge.getId(), sessionId);
} finally {
downlinkMsgLock.unlock();
}
@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable {
sendDownlinkMsg(edgeConfigMsg);
}
ListenableFuture<Void> processEdgeEvents() throws Exception {
SettableFuture<Void> result = SettableFuture.create();
ListenableFuture<Boolean> processEdgeEvents() throws Exception {
SettableFuture<Boolean> result = SettableFuture.create();
log.trace("[{}] starting processing edge events", this.sessionId);
if (isConnected() && isSyncCompleted()) {
Long queueStartTs = getQueueStartTs().get();
Pair<Long, Long> startTsAndSeqId = getQueueStartTsAndSeqId().get();
this.previousStartTs = startTsAndSeqId.getFirst();
this.previousStartSeqId = startTsAndSeqId.getSecond();
GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher(
queueStartTs,
this.previousStartTs,
this.previousStartSeqId,
this.seqIdEnd,
false,
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService());
ListenableFuture<UUID> ifOffsetFuture = startProcessingEdgeEvents(fetcher);
Futures.addCallback(ifOffsetFuture, new FutureCallback<>() {
Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable UUID ifOffset) {
if (ifOffset != null) {
Long newStartTs = Uuids.unixTimestamp(ifOffset);
ListenableFuture<List<String>> updateFuture = updateQueueStartTs(newStartTs);
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (newStartTsAndSeqId != null) {
ListenableFuture<List<String>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<String> list) {
log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs);
result.set(null);
log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId);
if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd();
boolean newEventsAvailable = isNewEdgeEventsAvailable();
result.set(newEventsAvailable);
} else {
seqIdEnd = null;
boolean newEventsAvailable = isSeqIdStartedNewCycle();
if (!newEventsAvailable) {
newEventsAvailable = isNewEdgeEventsAvailable();
}
result.set(newEventsAvailable);
}
}
@Override
public void onFailure(Throwable t) {
log.error("[{}] Failed to update queue offset [{}]", sessionId, ifOffset, t);
log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t);
result.setException(t);
}
}, ctx.getGrpcCallbackExecutorService());
} else {
log.trace("[{}] ifOffset is null. Skipping iteration without db update", sessionId);
log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId);
result.set(null);
}
}
@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable {
return result;
}
private ListenableFuture<UUID> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<UUID> result = SettableFuture.create();
private ListenableFuture<Pair<Long, Long>> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<Pair<Long, Long>> result = SettableFuture.create();
PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount());
processEdgeEvents(fetcher, pageLink, result);
return result;
}
private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<UUID> result) {
private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try {
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) {
@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
result.set(ifOffset);
EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1);
UUID idOffset = latestEdgeEvent.getUuidId();
if (idOffset != null) {
Long newStartTs = Uuids.unixTimestamp(idOffset);
long newStartSeqId = latestEdgeEvent.getSeqId();
result.set(Pair.of(newStartTs, newStartSeqId));
} else {
result.set(null);
}
}
}
}
@ -461,69 +491,113 @@ public final class EdgeGrpcSession implements Closeable {
}
}
private DownlinkMsg convertToDownlinkMsg(EdgeEvent edgeEvent) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
case UPDATED:
case ADDED:
case DELETED:
case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE:
case ALARM_ACK:
case ALARM_CLEAR:
case CREDENTIALS_UPDATED:
case RELATION_ADD_OR_UPDATE:
case RELATION_DELETED:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
case CREDENTIALS_REQUEST:
case RPC_CALL:
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
break;
case ATTRIBUTES_UPDATED:
case POST_ATTRIBUTES:
case ATTRIBUTES_DELETED:
case TIMESERIES_UPDATED:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
break;
default:
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
List<DownlinkMsg> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
case UPDATED:
case ADDED:
case DELETED:
case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE:
case ALARM_ACK:
case ALARM_CLEAR:
case CREDENTIALS_UPDATED:
case RELATION_ADD_OR_UPDATE:
case RELATION_DELETED:
case CREDENTIALS_REQUEST:
case RPC_CALL:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
break;
case ATTRIBUTES_UPDATED:
case POST_ATTRIBUTES:
case ATTRIBUTES_DELETED:
case TIMESERIES_UPDATED:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
break;
default:
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
}
} catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
}
if (downlinkMsg != null) {
result.add(downlinkMsg);
}
}
return result;
}
private ListenableFuture<Pair<Long, Long>> getQueueStartTsAndSeqId() {
ListenableFuture<List<AttributeKvEntry>> future =
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY));
return Futures.transform(future, attributeKvEntries -> {
long startTs = 0L;
long startSeqId = 0L;
for (AttributeKvEntry attributeKvEntry : attributeKvEntries) {
if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
}
if (startSeqId == 0L) {
startSeqId = findStartSeqIdFromOldestEventIfAny();
}
return Pair.of(startTs, startSeqId);
}, ctx.getGrpcCallbackExecutorService());
}
private boolean isSeqIdStartedNewCycle() {
try {
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink);
return !edgeEvents.getData().isEmpty();
} catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e);
}
return downlinkMsg;
return false;
}
private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
return edgeEvents
.stream()
.map(this::convertToDownlinkMsg)
.filter(Objects::nonNull)
.collect(Collectors.toList());
private boolean isNewEdgeEventsAvailable() {
try {
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink);
return !edgeEvents.getData().isEmpty();
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e);
}
return false;
}
private ListenableFuture<Long> getQueueStartTs() {
ListenableFuture<Optional<AttributeKvEntry>> future =
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY);
return Futures.transform(future, attributeKvEntryOpt -> {
if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) {
AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get();
return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
} else {
return 0L;
private long findStartSeqIdFromOldestEventIfAny() {
long startSeqId = 0L;
try {
TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null);
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink);
if (!edgeEvents.getData().isEmpty()) {
startSeqId = edgeEvents.getData().get(0).getSeqId() - 1;
}
}, ctx.getGrpcCallbackExecutorService());
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e);
}
return startSeqId;
}
private ListenableFuture<List<String>> updateQueueStartTs(Long newStartTs) {
log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs);
List<AttributeKvEntry> attributes = Collections.singletonList(
new BaseAttributeKvEntry(
new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()));
private ListenableFuture<List<String>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
this.newStartTs = pair.getFirst();
this.newStartSeqId = pair.getSecond();
log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
List<AttributeKvEntry> attributes = Arrays.asList(
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, this.newStartTs), System.currentTimeMillis()),
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, this.newStartSeqId), System.currentTimeMillis()));
return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes);
}
@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable {
}
private void interruptPreviousSendDownlinkMsgsTask() {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()
|| sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
}
}
private void interruptGeneralProcessingOnSync() {

18
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java

@ -18,7 +18,10 @@ package org.thingsboard.server.service.edge.rpc.constructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
@ -47,13 +50,22 @@ public class AlarmMsgConstructor {
String entityName = null;
switch (alarm.getOriginator().getEntityType()) {
case DEVICE:
entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName();
Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId()));
if (deviceById != null) {
entityName = deviceById.getName();
}
break;
case ASSET:
entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName();
Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId()));
if (assetById != null) {
entityName = assetById.getName();
}
break;
case ENTITY_VIEW:
entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName();
EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId()));
if (entityViewById != null) {
entityName = entityViewById.getName();
}
break;
}
AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder()

35
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java

@ -16,19 +16,27 @@
package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.edge.EdgeEventService;
@AllArgsConstructor
@Slf4j
public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs;
private Long seqIdStart;
@Getter
private Long seqIdEnd;
@Getter
private boolean seqIdNewCycleStarted;
private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService;
@Override
@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
pageSize,
0,
null,
new SortOrder("createdTime", SortOrder.Direction.ASC),
null,
queueStartTs,
null);
System.currentTimeMillis());
}
@Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
return edgeEventService.findEdgeEvents(tenantId, edge.getId(), (TimePageLink) pageLink, true);
try {
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) {
this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount);
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true;
this.seqIdStart = 0L;
} else {
edgeEvents = new PageData<>();
log.warn("[{}] unexpected edge notification message received. " +
"no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
}
}
return edgeEvents;
} catch (Exception e) {
log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
}
return new PageData<>();
}
}

5
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -709,4 +709,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
@Override
public void updateDefaultNotificationConfigs() {
notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
}
}

3
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -48,6 +48,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Optional;
import java.util.function.Function;
import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource;
@ -173,7 +174,7 @@ public class InstallScripts {
ruleChain = ruleChainService.saveRuleChain(ruleChain);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData);
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity());
return ruleChain;
}

12
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -744,6 +744,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} catch (Exception e) {
}
}
try {
conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
}

2
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -41,4 +41,6 @@ public interface SystemDataLoaderService {
void createDefaultNotificationConfigs();
void updateDefaultNotificationConfigs();
}

1
application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java

@ -19,4 +19,5 @@ public interface DataUpdateService {
void updateData(String fromVersion) throws Exception;
void upgradeRuleNodes();
}

81
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.install.update;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -26,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.query.DynamicValue;
@ -62,6 +65,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
@ -77,15 +81,18 @@ import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.service.install.TbRuleEngineQueueConfigService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank;
@ -133,7 +140,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
private QueueService queueService;
@Autowired
private TbRuleEngineQueueConfigService queueConfig;
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private SystemDataLoaderService systemDataLoaderService;
@ -195,19 +202,73 @@ public class DefaultDataUpdateService implements DataUpdateService {
} else {
log.info("Skipping audit logs migration");
}
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
if (!skipEdgeEventsMigration) {
log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
migrateEdgeEvents("Starting edge events migration. ");
break;
case "3.5.1":
log.info("Updating data from version 3.5.1 to 3.5.2 ...");
migrateEdgeEvents("Starting edge events migration - adding seq_id column. ");
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
private void migrateEdgeEvents(String logPrefix) {
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
if (!skipEdgeEventsMigration) {
log.info(logPrefix + "Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
}
@Override
public void upgradeRuleNodes() {
try {
log.info("Lookup rule nodes to upgrade ...");
var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes();
log.info("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size());
nodeClassToVersionMap.forEach(clazz -> {
var ruleNodeType = clazz.getClassName();
var ruleNodeTypeForLogs = clazz.getSimpleName();
var toVersion = clazz.getCurrentVersion();
log.info("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion);
var ruleNodesToUpdate = new PageDataIterable<>(
pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024
);
if (Iterables.isEmpty(ruleNodesToUpdate)) {
log.info("There are no active nodes with type: {}, or all nodes with this type already set to latest version!", ruleNodeTypeForLogs);
} else {
for (var ruleNode : ruleNodesToUpdate) {
var ruleNodeId = ruleNode.getId();
var oldConfiguration = ruleNode.getConfiguration();
int fromVersion = ruleNode.getConfigurationVersion();
log.info("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
try {
var tbVersionedNode = (TbVersionedNode) clazz.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
if (upgradeRuleNodeConfigurationResult.getFirst()) {
ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
}
ruleNode.setConfigurationVersion(toVersion);
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.info("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
} catch (Exception e) {
log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e);
}
}
}
});
log.info("Finished rule nodes upgrade!");
} catch (Exception e) {
log.error("Unexpected error during rule nodes upgrade: ", e);
}
}
private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater =
new PaginatedUpdater<>() {
@ -442,7 +503,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
md.getNodes().add(ruleNode);
md.setFirstNodeIndex(newIdx);
md.addConnectionInfo(newIdx, oldIdx, "Success");
ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
ruleChainService.saveRuleChainMetaData(tenant.getId(), md, Function.identity());
}
} catch (Exception e) {
log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e);

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

@ -16,6 +16,7 @@
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.xml.bind.DatatypeConverter;
@ -54,7 +55,6 @@ import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -62,7 +62,6 @@ import java.util.concurrent.TimeoutException;
@Slf4j
public class DefaultMailService implements MailService {
public static final String MAIL_PROP = "mail.";
public static final String TARGET_EMAIL = "targetEmail";
public static final String UTF_8 = "UTF-8";
@ -83,7 +82,10 @@ public class DefaultMailService implements MailService {
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
private JavaMailSenderImpl mailSender;
@Autowired
private TbMailContextComponent tbMailContextComponent;
private TbMailSender mailSender;
private String mailFrom;
@ -106,7 +108,7 @@ public class DefaultMailService implements MailService {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null) {
JsonNode jsonConfig = settings.getJsonValue();
mailSender = createMailSender(jsonConfig);
mailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
mailFrom = jsonConfig.get("mailFrom").asText();
timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);
} else {
@ -114,65 +116,6 @@ public class DefaultMailService implements MailService {
}
}
private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(jsonConfig.get("smtpHost").asText());
mailSender.setPort(parsePort(jsonConfig.get("smtpPort").asText()));
mailSender.setUsername(jsonConfig.get("username").asText());
mailSender.setPassword(jsonConfig.get("password").asText());
mailSender.setJavaMailProperties(createJavaMailProperties(jsonConfig));
return mailSender;
}
private Properties createJavaMailProperties(JsonNode jsonConfig) {
Properties javaMailProperties = new Properties();
String protocol = jsonConfig.get("smtpProtocol").asText();
javaMailProperties.put("mail.transport.protocol", protocol);
javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText())));
boolean enableTls = false;
if (jsonConfig.has("enableTls")) {
if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) {
enableTls = true;
} else if (jsonConfig.get("enableTls").isTextual()) {
enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText());
}
}
javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls);
if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) {
String tlsVersion = jsonConfig.get("tlsVersion").asText();
if (StringUtils.isNoneEmpty(tlsVersion)) {
javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion);
}
}
boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean();
if (enableProxy) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText());
String proxyUser = jsonConfig.get("proxyUser").asText();
if (StringUtils.isNoneEmpty(proxyUser)) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser);
}
String proxyPassword = jsonConfig.get("proxyPassword").asText();
if (StringUtils.isNoneEmpty(proxyPassword)) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword);
}
}
return javaMailProperties;
}
private int parsePort(String strPort) {
try {
return Integer.valueOf(strPort);
} catch (NumberFormatException e) {
throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
}
}
@Override
public void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException {
sendMail(mailSender, mailFrom, email, subject, message, timeout);
@ -180,7 +123,7 @@ public class DefaultMailService implements MailService {
@Override
public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
TbMailSender testMailSender = new TbMailSender(tbMailContextComponent, jsonConfig);
String mailFrom = jsonConfig.get("mailFrom").asText();
String subject = messages.getMessage("test.message.subject", null, Locale.US);
long timeout = jsonConfig.get("timeout").asLong(DEFAULT_TIMEOUT);

42
application/src/main/java/org/thingsboard/server/service/mail/DefaultTbMailConfigTemplateService.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import javax.annotation.PostConstruct;
import java.io.IOException;
@Service
@Slf4j
public class DefaultTbMailConfigTemplateService implements TbMailConfigTemplateService {
private JsonNode mailConfigTemplates;
@PostConstruct
private void postConstruct() throws IOException {
mailConfigTemplates = JacksonUtil.toJsonNode(new ClassPathResource("/templates/mail_config_templates.json").getFile());
}
@Override
public JsonNode findAllMailConfigTemplates() {
return mailConfigTemplates;
}
}

75
application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java

@ -0,0 +1,75 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.RefreshTokenRequest;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import static org.thingsboard.server.common.data.mail.MailOauth2Provider.OFFICE_365;
@TbCoreComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class RefreshTokenExpCheckService {
public static final int AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS = 90;
private final AdminSettingsService adminSettingsService;
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${mail.oauth2.refreshTokenCheckingInterval})}", fixedDelayString = "${mail.oauth2.refreshTokenCheckingInterval}")
public void check() throws IOException {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null && settings.getJsonValue().has("enableOauth2") && settings.getJsonValue().get("enableOauth2").asBoolean()) {
JsonNode jsonValue = settings.getJsonValue();
if (OFFICE_365.name().equals(jsonValue.get("providerId").asText()) && jsonValue.has("refreshTokenExpires")) {
long expiresIn = jsonValue.get("refreshTokenExpires").longValue();
if ((expiresIn - System.currentTimeMillis()) < 604800000L) { //less than 7 days
log.info("Trying to refresh refresh token.");
String clientId = jsonValue.get("clientId").asText();
String clientSecret = jsonValue.get("clientSecret").asText();
String refreshToken = jsonValue.get("refreshToken").asText();
String tokenUri = jsonValue.get("tokenUri").asText();
TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(),
new GenericUrl(tokenUri), refreshToken)
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute();
((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
}
}
}
}
}

24
application/src/main/java/org/thingsboard/server/service/mail/TbMailConfigTemplateService.java

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

32
application/src/main/java/org/thingsboard/server/service/mail/TbMailContextComponent.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@Data
@Lazy
public class TbMailContextComponent {
@Autowired
private AdminSettingsService adminSettingsService;
}

168
application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java

@ -0,0 +1,168 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.mail;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.RefreshTokenRequest;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mail.MailOauth2Provider;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import jakarta.mail.internet.MimeMessage;
import java.time.Duration;
import java.time.Instant;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.thingsboard.server.service.mail.RefreshTokenExpCheckService.AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS;
@Slf4j
public class TbMailSender extends JavaMailSenderImpl {
private static final String MAIL_PROP = "mail.";
private final TbMailContextComponent ctx;
private final Lock lock;
private final Boolean oauth2Enabled;
private volatile String accessToken;
private volatile long tokenExpires;
public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) {
super();
this.lock = new ReentrantLock();
this.tokenExpires = 0L;
this.ctx = ctx;
this.oauth2Enabled = jsonConfig.has("enableOauth2") && jsonConfig.get("enableOauth2").asBoolean();
setHost(jsonConfig.get("smtpHost").asText());
setPort(parsePort(jsonConfig.get("smtpPort").asText()));
setUsername(jsonConfig.get("username").asText());
if (jsonConfig.has("password")) {
setPassword(jsonConfig.get("password").asText());
}
setJavaMailProperties(createJavaMailProperties(jsonConfig));
}
@SneakyThrows
@Override
public void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) {
if (oauth2Enabled && (System.currentTimeMillis() > tokenExpires)){
refreshAccessToken();
setPassword(accessToken);
}
super.doSend(mimeMessages, originalMessages);
}
private Properties createJavaMailProperties(JsonNode jsonConfig) {
Properties javaMailProperties = new Properties();
String protocol = jsonConfig.get("smtpProtocol").asText();
javaMailProperties.put("mail.transport.protocol", protocol);
javaMailProperties.put(MAIL_PROP + protocol + ".host", jsonConfig.get("smtpHost").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".port", jsonConfig.get("smtpPort").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".timeout", jsonConfig.get("timeout").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".auth", String.valueOf(StringUtils.isNotEmpty(jsonConfig.get("username").asText())));
boolean enableTls = false;
if (jsonConfig.has("enableTls")) {
if (jsonConfig.get("enableTls").isBoolean() && jsonConfig.get("enableTls").booleanValue()) {
enableTls = true;
} else if (jsonConfig.get("enableTls").isTextual()) {
enableTls = "true".equalsIgnoreCase(jsonConfig.get("enableTls").asText());
}
}
javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", enableTls);
if (enableTls && jsonConfig.has("tlsVersion") && !jsonConfig.get("tlsVersion").isNull()) {
String tlsVersion = jsonConfig.get("tlsVersion").asText();
if (StringUtils.isNoneEmpty(tlsVersion)) {
javaMailProperties.put(MAIL_PROP + protocol + ".ssl.protocols", tlsVersion);
}
}
boolean enableProxy = jsonConfig.has("enableProxy") && jsonConfig.get("enableProxy").asBoolean();
if (enableProxy) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.host", jsonConfig.get("proxyHost").asText());
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.port", jsonConfig.get("proxyPort").asText());
String proxyUser = jsonConfig.get("proxyUser").asText();
if (StringUtils.isNoneEmpty(proxyUser)) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.user", proxyUser);
}
String proxyPassword = jsonConfig.get("proxyPassword").asText();
if (StringUtils.isNoneEmpty(proxyPassword)) {
javaMailProperties.put(MAIL_PROP + protocol + ".proxy.password", proxyPassword);
}
}
if (oauth2Enabled) {
javaMailProperties.put(MAIL_PROP + protocol + ".auth.mechanisms", "XOAUTH2");
}
return javaMailProperties;
}
public void refreshAccessToken() throws ThingsboardException {
lock.lock();
try {
if (System.currentTimeMillis() > tokenExpires) {
AdminSettings settings = ctx.getAdminSettingsService().findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
JsonNode jsonValue = settings.getJsonValue();
String clientId = jsonValue.get("clientId").asText();
String clientSecret = jsonValue.get("clientSecret").asText();
String refreshToken = jsonValue.get("refreshToken").asText();
String tokenUri = jsonValue.get("tokenUri").asText();
String providerId = jsonValue.get("providerId").asText();
TokenResponse tokenResponse = new RefreshTokenRequest(new NetHttpTransport(), new GsonFactory(),
new GenericUrl(tokenUri), refreshToken)
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute();
if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) {
((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
}
accessToken = tokenResponse.getAccessToken();
tokenExpires = System.currentTimeMillis() + (tokenResponse.getExpiresInSeconds().intValue() * 1000);
}
} catch (Exception e) {
log.warn("Unable to retrieve access token: {}", e.getMessage());
throw new ThingsboardException("Error while retrieving access token: " + e.getMessage(), ThingsboardErrorCode.GENERAL);
} finally {
lock.unlock();
}
}
private int parsePort(String strPort) {
try {
return Integer.parseInt(strPort);
} catch (NumberFormatException e) {
throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
}
}
}

103
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -15,13 +15,10 @@
*/
package org.thingsboard.server.service.notification;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -31,6 +28,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -59,14 +57,11 @@ import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.NotificationsTopicService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.channels.NotificationChannel;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -74,7 +69,6 @@ import org.thingsboard.server.service.telemetry.AbstractSubscriptionService;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -87,7 +81,7 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({"UnstableApiUsage", "rawtypes"})
@SuppressWarnings({"rawtypes"})
public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel<User, WebDeliveryMethodNotificationTemplate> {
private final NotificationTargetService notificationTargetService;
@ -95,9 +89,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private final NotificationService notificationService;
private final NotificationTemplateService notificationTemplateService;
private final NotificationSettingsService notificationSettingsService;
private final UserService userService;
private final NotificationExecutorService notificationExecutor;
private final DbCallbackExecutorService dbCallbackExecutorService;
private final NotificationsTopicService notificationsTopicService;
private final TbQueueProducerProvider producerProvider;
private final RateLimitService rateLimitService;
@ -172,37 +164,32 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.build();
notificationExecutor.submit(() -> {
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationTarget target : targets) {
List<ListenableFuture<Void>> result = processForTarget(target, ctx);
results.addAll(result);
processForTarget(target, ctx);
}
Futures.whenAllComplete(results).run(() -> {
NotificationRequestId requestId = ctx.getRequest().getId();
log.debug("[{}] Notification request processing is finished", requestId);
NotificationRequestStats stats = ctx.getStats();
NotificationRequestId requestId = ctx.getRequest().getId();
log.debug("[{}] Notification request processing is finished", requestId);
NotificationRequestStats stats = ctx.getStats();
try {
notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats);
} catch (Exception e) {
log.error("[{}] Failed to update stats for notification request", requestId, e);
}
if (callback != null) {
try {
notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats);
callback.accept(stats);
} catch (Exception e) {
log.error("[{}] Failed to update stats for notification request", requestId, e);
}
if (callback != null) {
try {
callback.accept(stats);
} catch (Exception e) {
log.error("Failed to process callback for notification request {}", requestId, e);
}
log.error("Failed to process callback for notification request {}", requestId, e);
}
}, dbCallbackExecutorService);
}
});
return request;
}
private List<ListenableFuture<Void>> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
private void processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
Iterable<? extends NotificationRecipient> recipients;
switch (target.getConfiguration().getType()) {
case PLATFORM_USERS: {
@ -231,43 +218,35 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>(ctx.getDeliveryMethods());
deliveryMethods.removeIf(deliveryMethod -> !target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod));
log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods);
if (deliveryMethods.isEmpty()) {
return;
}
List<ListenableFuture<Void>> results = new ArrayList<>();
if (!deliveryMethods.isEmpty()) {
for (NotificationRecipient recipient : recipients) {
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
ListenableFuture<Void> resultFuture = processForRecipient(deliveryMethod, recipient, ctx);
DonAsynchron.withCallback(resultFuture, result -> {
ctx.getStats().reportSent(deliveryMethod, recipient);
}, error -> {
ctx.getStats().reportError(deliveryMethod, error, recipient);
});
results.add(resultFuture);
for (NotificationRecipient recipient : recipients) {
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
try {
processForRecipient(deliveryMethod, recipient, ctx);
ctx.getStats().reportSent(deliveryMethod, recipient);
} catch (Exception error) {
ctx.getStats().reportError(deliveryMethod, error, recipient);
}
}
}
return results;
}
private ListenableFuture<Void> processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) {
private void processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) throws Exception {
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) {
return Futures.immediateFailedFuture(new AlreadySentException());
throw new AlreadySentException();
}
DeliveryMethodNotificationTemplate processedTemplate;
try {
processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
NotificationChannel notificationChannel = channels.get(deliveryMethod);
DeliveryMethodNotificationTemplate processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient);
log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient);
return notificationChannel.sendNotification(recipient, processedTemplate, ctx);
notificationChannel.sendNotification(recipient, processedTemplate, ctx);
}
@Override
public ListenableFuture<Void> sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
public void sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
NotificationRequest request = ctx.getRequest();
Notification notification = Notification.builder()
.requestId(request.getId())
@ -283,14 +262,14 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
notification = notificationService.saveNotification(recipient.getTenantId(), notification);
} catch (Exception e) {
log.error("Failed to create notification for recipient {}", recipient.getId(), e);
return Futures.immediateFailedFuture(e);
throw e;
}
NotificationUpdate update = NotificationUpdate.builder()
.created(true)
.notification(notification)
.build();
return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
}
@Override
@ -384,13 +363,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null);
}
private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
private void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
log.trace("Submitting notification update for recipient {}: {}", recipientId, update);
return Futures.submit(() -> {
forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY);
}, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update));
}, wsCallBackExecutor);
forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY);
}, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update));
}
private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) {

20
application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
@ -24,7 +23,6 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component
@ -32,19 +30,15 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
public class EmailNotificationChannel implements NotificationChannel<User, EmailDeliveryMethodNotificationTemplate> {
private final MailService mailService;
private final MailExecutorService executor;
@Override
public ListenableFuture<Void> sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
return executor.submit(() -> {
mailService.send(recipient.getTenantId(), null, TbEmail.builder()
.to(recipient.getEmail())
.subject(processedTemplate.getSubject())
.body(processedTemplate.getBody())
.html(true)
.build());
return null;
});
public void sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
mailService.send(recipient.getTenantId(), null, TbEmail.builder()
.to(recipient.getEmail())
.subject(processedTemplate.getSubject())
.body(processedTemplate.getBody())
.html(true)
.build());
}
@Override

3
application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
@ -24,7 +23,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
public interface NotificationChannel<R extends NotificationRecipient, T extends DeliveryMethodNotificationTemplate> {
ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
void sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx) throws Exception;
void check(TenantId tenantId) throws Exception;

10
application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.slack.SlackService;
@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.notification.settings.SlackNotificatio
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@Component
@ -35,15 +33,11 @@ public class SlackNotificationChannel implements NotificationChannel<SlackConver
private final SlackService slackService;
private final NotificationSettingsService notificationSettingsService;
private final ExternalCallExecutorService executor;
@Override
public ListenableFuture<Void> sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
public void sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK);
return executor.submit(() -> {
slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody());
return null;
});
slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody());
}
@Override

13
application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -26,26 +24,21 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.sms.SmsExecutorService;
@Component
@RequiredArgsConstructor
public class SmsNotificationChannel implements NotificationChannel<User, SmsDeliveryMethodNotificationTemplate> {
private final SmsService smsService;
private final SmsExecutorService executor;
@Override
public ListenableFuture<Void> sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
public void sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
String phone = recipient.getPhone();
if (StringUtils.isBlank(phone)) {
return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number"));
throw new RuntimeException("User does not have phone number");
}
return executor.submit(() -> {
smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
return null;
});
smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
}
@Override

102
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -15,52 +15,44 @@
*/
package org.thingsboard.server.service.notification.rule;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.notification.NotificationDeduplicationService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.rule.cache.NotificationRulesCache;
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor;
import jakarta.annotation.PostConstruct;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -72,41 +64,39 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationRulesCache notificationRulesCache;
private final NotificationRequestService notificationRequestService;
private final NotificationDeduplicationService deduplicationService;
private final PartitionService partitionService;
private final RateLimitService rateLimitService;
@Autowired @Lazy
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
private final CacheManager cacheManager;
private Cache sentNotifications;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
@PostConstruct
private void init() {
sentNotifications = cacheManager.getCache(CacheConstants.SENT_NOTIFICATIONS_CACHE);
if (sentNotifications == null) {
throw new IllegalStateException("Sent notifications cache is not set up");
}
}
@Override
public void process(NotificationRuleTrigger trigger) {
NotificationRuleTriggerType triggerType = trigger.getType();
if (triggerType == null) return;
TenantId tenantId = triggerType.isTenantLevel() ? trigger.getTenantId() : TenantId.SYS_TENANT_ID;
try {
List<NotificationRule> rules = notificationRulesCache.getEnabled(tenantId, triggerType);
for (NotificationRule rule : rules) {
notificationExecutor.submit(() -> {
List<NotificationRule> enabledRules = notificationRulesCache.getEnabled(tenantId, triggerType);
if (enabledRules.isEmpty()) {
return;
}
if (trigger.deduplicate()) {
enabledRules = new ArrayList<>(enabledRules);
enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule));
}
final List<NotificationRule> rules = enabledRules;
notificationExecutor.submit(() -> {
for (NotificationRule rule : rules) {
try {
processNotificationRule(rule, trigger);
} catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
}
});
}
}
});
} catch (Throwable e) {
log.error("Failed to process notification rules for trigger: {}", trigger, e);
}
@ -142,9 +132,6 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return;
}
if (trigger.getType().isDeduplicate() && alreadySent(rule.getId(), trigger)) {
return;
}
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
@ -172,14 +159,13 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
.ruleId(rule.getId())
.originatorEntityId(originatorEntityId)
.build();
notificationExecutor.submit(() -> {
try {
log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets);
notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null);
} catch (Exception e) {
log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e);
}
});
try {
log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets);
notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest, null);
} catch (Exception e) {
log.error("Failed to process notification request for tenant {} for rule {}", rule.getTenantId(), rule.getId(), e);
}
}
private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) {
@ -194,23 +180,6 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger);
}
private boolean alreadySent(NotificationRuleId ruleId, NotificationRuleTrigger trigger) {
String key = ruleId + "_" + trigger.getOriginatorEntityId();
SentNotification sent = sentNotifications.get(key, SentNotification.class);
boolean alreadySent;
if (sent != null && sent.getTrigger().equals(trigger)) {
alreadySent = true;
log.debug("Notification for {} trigger was already sent, ignoring", trigger.getType());
// updating cache anyway so that the value is not removed by ttl
} else {
alreadySent = false;
sent = new SentNotification(trigger);
}
log.trace("[{}] Putting to sentNotifications cache: {}", ruleId, trigger);
sentNotifications.put(key, sent);
return alreadySent;
}
@EventListener(ComponentLifecycleMsg.class)
public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) {
if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED ||
@ -232,24 +201,9 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
@Autowired
public void setTriggerProcessors(Collection<NotificationRuleTriggerProcessor> processors) {
Map<String, NotificationRuleTriggerType> ruleEngineMsgTypeToTriggerType = new HashMap<>();
processors.forEach(processor -> {
triggerProcessors.put(processor.getTriggerType(), processor);
if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) {
Set<String> supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor<?>) processor).getSupportedMsgTypes();
supportedMsgTypes.forEach(supportedMsgType -> {
ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType());
});
}
});
RuleEngineMsgTrigger.msgTypeToTriggerType = ruleEngineMsgTypeToTriggerType;
}
@Data
private static class SentNotification implements Serializable {
private static final long serialVersionUID = 38973480405095422L;
private final NotificationRuleTrigger trigger;
}
}

17
application/src/main/java/org/thingsboard/server/service/notification/rule/cache/DefaultNotificationRulesCache.java

@ -17,7 +17,6 @@ package org.thingsboard.server.service.notification.rule.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -25,7 +24,7 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.notification.NotificationRuleService;
@ -50,7 +49,7 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache {
private int cacheMaxSize;
@Value("${cache.notificationRules.timeToLiveInMinutes:30}")
private int cacheValueTtl;
private Cache<CacheKey, List<NotificationRule>> cache;
private Cache<String, List<NotificationRule>> cache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
@ -96,21 +95,15 @@ public class DefaultNotificationRulesCache implements NotificationRulesCache {
}
}
private void evict(TenantId tenantId) {
public void evict(TenantId tenantId) {
cache.invalidateAll(Arrays.stream(NotificationRuleTriggerType.values())
.map(triggerType -> key(tenantId, triggerType))
.collect(Collectors.toList()));
log.trace("Evicted all notification rules for tenant {} from cache", tenantId);
}
private static CacheKey key(TenantId tenantId, NotificationRuleTriggerType triggerType) {
return new CacheKey(tenantId, triggerType);
}
@Data
private static class CacheKey {
private final TenantId tenantId;
private final NotificationRuleTriggerType triggerType;
private static String key(TenantId tenantId, NotificationRuleTriggerType triggerType) {
return tenantId + "_" + triggerType;
}
}

2
application/src/main/java/org/thingsboard/server/service/notification/rule/cache/NotificationRulesCache.java

@ -17,7 +17,7 @@ package org.thingsboard.server.service.notification.rule.cache;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.List;

45
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -16,52 +16,48 @@
package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssignee;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.AlarmAssignmentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import java.util.Set;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig.Action;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmAssignmentTrigger, AlarmAssignmentNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED;
public boolean matchesFilter(AlarmAssignmentTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
Action action = trigger.getActionType() == ActionType.ALARM_ASSIGNED ? Action.ASSIGNED : Action.UNASSIGNED;
if (!triggerConfig.getNotifyOn().contains(action)) {
return false;
}
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
AlarmInfo alarmInfo = trigger.getAlarmInfo();
return emptyOrContains(triggerConfig.getAlarmTypes(), alarmInfo.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarmInfo.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarmInfo));
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class);
public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmAssignmentTrigger trigger) {
AlarmInfo alarmInfo = trigger.getAlarmInfo();
AlarmAssignee assignee = alarmInfo.getAssignee();
return AlarmAssignmentNotificationInfo.builder()
.action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned")
.action(trigger.getActionType() == ActionType.ALARM_ASSIGNED ? "assigned" : "unassigned")
.assigneeFirstName(assignee != null ? assignee.getFirstName() : null)
.assigneeLastName(assignee != null ? assignee.getLastName() : null)
.assigneeEmail(assignee != null ? assignee.getEmail() : null)
.assigneeId(assignee != null ? assignee.getId() : null)
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName"))
.userEmail(trigger.getUser().getEmail())
.userFirstName(trigger.getUser().getFirstName())
.userLastName(trigger.getUser().getLastName())
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
@ -77,9 +73,4 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED);
}
}

74
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -15,68 +15,67 @@
*/
package org.thingsboard.server.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import java.util.Set;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger;
import org.thingsboard.server.dao.entity.EntityService;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@RequiredArgsConstructor
public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmCommentTrigger, AlarmCommentNotificationRuleTriggerConfig> {
private final EntityService entityService;
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
TbMsg msg = trigger.getMsg();
if (msg.getMetaData().getValue("comment") == null) {
return false;
}
if (msg.getType().equals(DataConstants.COMMENT_UPDATED) && !triggerConfig.isNotifyOnCommentUpdate()) {
public boolean matchesFilter(AlarmCommentTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
if (trigger.getActionType() == ActionType.UPDATED_COMMENT && !triggerConfig.isNotifyOnCommentUpdate()) {
return false;
}
if (triggerConfig.isOnlyUserComments()) {
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
if (comment.getType() == AlarmCommentType.SYSTEM) {
if (trigger.getComment().getType() == AlarmCommentType.SYSTEM) {
return false;
}
}
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
Alarm alarm = trigger.getAlarm();
return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
TbMsg msg = trigger.getMsg();
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class);
public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) {
Alarm alarm = trigger.getAlarm();
String originatorName;
if (alarm instanceof AlarmInfo) {
originatorName = ((AlarmInfo) alarm).getOriginatorName();
} else {
originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse("");
}
return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userEmail(msg.getMetaData().getValue("userEmail"))
.userFirstName(msg.getMetaData().getValue("userFirstName"))
.userLastName(msg.getMetaData().getValue("userLastName"))
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())
.comment(trigger.getComment().getComment().get("text").asText())
.action(trigger.getActionType() == ActionType.ADDED_COMMENT ? "added" : "updated")
.userEmail(trigger.getUser().getEmail())
.userFirstName(trigger.getUser().getFirstName())
.userLastName(trigger.getUser().getLastName())
.alarmId(alarm.getUuidId())
.alarmType(alarm.getType())
.alarmOriginator(alarm.getOriginator())
.alarmOriginatorName(originatorName)
.alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus())
.alarmCustomerId(alarm.getCustomerId())
.build();
}
@ -85,9 +84,4 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return NotificationRuleTriggerType.ALARM_COMMENT;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED);
}
}

12
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -22,11 +22,11 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@ -108,6 +108,8 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId())
.build();
}

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java

@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.ApiUsageLimitNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.dao.tenant.TenantService;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;

44
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceActivityTriggerProcessor.java

@ -18,38 +18,30 @@ package org.thingsboard.server.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.DeviceActivityNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<DeviceActivityNotificationRuleTriggerConfig> {
public class DeviceActivityTriggerProcessor implements NotificationRuleTriggerProcessor<DeviceActivityTrigger, DeviceActivityNotificationRuleTriggerConfig> {
private final TbDeviceProfileCache deviceProfileCache;
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
if (trigger.getMsg().getOriginator().getEntityType() != EntityType.DEVICE) {
return false;
}
DeviceEvent event = trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
public boolean matchesFilter(DeviceActivityTrigger trigger, DeviceActivityNotificationRuleTriggerConfig triggerConfig) {
DeviceEvent event = trigger.isActive() ? DeviceEvent.ACTIVE : DeviceEvent.INACTIVE;
if (!triggerConfig.getNotifyOn().contains(event)) {
return false;
}
DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator();
DeviceId deviceId = trigger.getDeviceId();
if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) {
return triggerConfig.getDevices().contains(deviceId.getId());
} else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) {
@ -61,15 +53,14 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) {
TbMsg msg = trigger.getMsg();
public RuleOriginatedNotificationInfo constructNotificationInfo(DeviceActivityTrigger trigger) {
return DeviceActivityNotificationInfo.builder()
.eventType(trigger.getMsg().getType().equals(DataConstants.ACTIVITY_EVENT) ? "active" : "inactive")
.deviceId(msg.getOriginator().getId())
.deviceName(msg.getMetaData().getValue("deviceName"))
.deviceType(msg.getMetaData().getValue("deviceType"))
.deviceLabel(msg.getMetaData().getValue("deviceLabel"))
.deviceCustomerId(msg.getCustomerId())
.eventType(trigger.isActive() ? "active" : "inactive")
.deviceId(trigger.getDeviceId().getId())
.deviceName(trigger.getDeviceName())
.deviceType(trigger.getDeviceType())
.deviceLabel(trigger.getDeviceLabel())
.deviceCustomerId(trigger.getCustomerId())
.build();
}
@ -78,9 +69,4 @@ public class DeviceActivityTriggerProcessor implements RuleEngineMsgNotification
return NotificationRuleTriggerType.DEVICE_ACTIVITY;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT);
}
}

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java

@ -19,10 +19,10 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -20,9 +20,9 @@ import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java

@ -20,9 +20,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
@Service
@RequiredArgsConstructor

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java

@ -16,9 +16,9 @@
package org.thingsboard.server.service.notification.rule.trigger;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
public interface NotificationRuleTriggerProcessor<T extends NotificationRuleTrigger, C extends NotificationRuleTriggerConfig> {

71
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RateLimitsTriggerProcessor.java

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.RateLimitsNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RateLimitsNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class RateLimitsTriggerProcessor implements NotificationRuleTriggerProcessor<RateLimitsTrigger, RateLimitsNotificationRuleTriggerConfig> {
private final TenantService tenantService;
private final EntityService entityService;
@Override
public boolean matchesFilter(RateLimitsTrigger trigger, RateLimitsNotificationRuleTriggerConfig triggerConfig) {
return trigger.getLimitLevel() != null && trigger.getApi().getLabel() != null &&
CollectionsUtil.emptyOrContains(triggerConfig.getApis(), trigger.getApi());
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RateLimitsTrigger trigger) {
EntityId limitLevel = trigger.getLimitLevel();
String tenantName = tenantService.findTenantById(trigger.getTenantId()).getName();
String limitLevelEntityName = null;
if (limitLevel instanceof TenantId) {
limitLevelEntityName = tenantName;
} else if (limitLevel != null) {
limitLevelEntityName = Optional.ofNullable(trigger.getLimitLevelEntityName())
.orElseGet(() -> entityService.fetchEntityName(trigger.getTenantId(), limitLevel).orElse(null));
}
return RateLimitsNotificationInfo.builder()
.tenantId(trigger.getTenantId())
.tenantName(tenantName)
.api(trigger.getApi())
.limitLevel(limitLevel)
.limitLevelEntityName(limitLevelEntityName)
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RATE_LIMITS;
}
}

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java

@ -23,10 +23,10 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
@ -64,7 +64,7 @@ import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;

16
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -258,7 +258,21 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
}
void launchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}
private void scheduleLaunchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
repartitionExecutor.schedule(() -> {
if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}, 10, TimeUnit.SECONDS);
}
void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {

3
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -68,7 +68,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService notificationsConsumerExecutor;
protected volatile boolean stopped = false;
protected volatile boolean isReady = false;
protected final ActorSystemContext actorContext;
protected final DataDecodingEncodingService encodingService;
protected final TbTenantProfileCache tenantProfileCache;
@ -108,6 +108,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
this.nfConsumer.subscribe();
this.isReady = true;
launchNotificationsConsumer();
launchMainConsumers();
}

14
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.resource;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -22,6 +24,7 @@ import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -35,6 +38,7 @@ import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@ -72,13 +76,13 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
return resourceService.findTenantResourcesByTenantId(tenantId, pageLink);
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findTenantResourcesByTenantId(filter, pageLink);
}
@Override
@ -165,6 +169,8 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
} else {
resource.setResourceKey(resource.getFileName());
}
HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes()));
resource.setEtag(hashCode.toString());
return resourceService.saveResource(resource);
}
}

5
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@ -35,9 +36,9 @@ public interface TbResourceService extends SimpleTbEntityService<TbResource> {
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List<LwM2mObject> findLwM2mObject(TenantId tenantId,
String sortOrder,

50
application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java

@ -15,10 +15,13 @@
*/
package org.thingsboard.server.service.rule;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode;
@ -42,12 +45,13 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.ArrayList;
import java.util.Collections;
@ -70,8 +74,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
private final RuleChainService ruleChainService;
private final RelationService relationService;
private final InstallScripts installScripts;
private final EntitiesVersionControlService vcService;
private final ComponentDiscoveryService componentDiscoveryService;
@Override
public Set<String> getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) {
@ -277,7 +280,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
RuleChainId ruleChainId = ruleChain.getId();
RuleChainId ruleChainMetaDataId = ruleChainMetaData.getRuleChainId();
try {
RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData);
RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, this::updateRuleNodeConfiguration);
checkNotNull(result.isSuccess() ? true : null);
List<RuleChain> updatedRuleChains;
@ -404,6 +407,45 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
}
}
@Override
public RuleNode updateRuleNodeConfiguration(RuleNode node) {
var ruleChainId = node.getRuleChainId();
var ruleNodeId = node.getId();
var ruleNodeType = node.getType();
try {
var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType)
.orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!"));
if (ruleNodeClass.isVersioned()) {
TbVersionedNode tbVersionedNode = (TbVersionedNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
int fromVersion = node.getConfigurationVersion();
int toVersion = ruleNodeClass.getCurrentVersion();
if (fromVersion < toVersion) {
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, fromVersion, toVersion);
try {
TbPair<Boolean, JsonNode> upgradeResult = tbVersionedNode.upgrade(fromVersion, node.getConfiguration());
if (upgradeResult.getFirst()) {
node.setConfiguration(upgradeResult.getSecond());
}
node.setConfigurationVersion(toVersion);
log.debug("Successfully upgrade rule node with id: {} type: {}, rule chain id: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion);
} catch (TbNodeException e) {
log.warn("Failed to upgrade rule node with id: {} type: {} rule chain id: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion, e);
}
} else {
log.debug("Rule node with id: {} type: {} ruleChainId: {} already set to latest version!",
ruleNodeId, ruleChainId, ruleNodeType);
}
}
} catch (Exception e) {
log.error("Failed to update the rule node with id: {} type: {}, rule chain id: {}",
ruleNodeId, ruleNodeType, ruleChainId, e);
}
return node;
}
private Set<RuleChainId> updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, Map<String, String> labelsMap) {
Set<RuleChainId> updatedRuleChains = new HashSet<>();
List<RuleChainOutputLabelsUsage> usageList = getOutputLabelUsage(tenantId, ruleChainId);

3
application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import java.util.List;
@ -54,4 +55,6 @@ public interface TbRuleChainService extends SimpleTbEntityService<RuleChain> {
RuleChain setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
RuleChain unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
RuleNode updateRuleNodeConfiguration(RuleNode ruleNode);
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccoun
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;

25
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java

@ -19,9 +19,13 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -44,6 +48,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.RPC, rpcPermissionChecker);
put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker);
put(Resource.TB_RESOURCE, customerResourcePermissionChecker);
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {
@ -95,6 +100,26 @@ public class CustomerUserPermissions extends AbstractPermissions {
};
private static final PermissionChecker customerResourcePermissionChecker =
new PermissionChecker<TbResourceId, TbResourceInfo>() {
@Override
@SuppressWarnings("unchecked")
public boolean hasPermission(SecurityUser user, Operation operation, TbResourceId resourceId, TbResourceInfo resource) {
if (operation != Operation.READ) {
return false;
}
if (resource.getResourceType() == null || !resource.getResourceType().isCustomerAccess()) {
return false;
}
if (resource.getTenantId() == null || resource.getTenantId().isNullUid()) {
return true;
}
return user.getTenantId().equals(resource.getTenantId());
}
};
private static final PermissionChecker customerDashboardPermissionChecker =
new PermissionChecker.GenericPermissionChecker<DashboardId, DashboardInfo>(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) {

22
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -62,6 +62,8 @@ import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -155,6 +157,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private final EntityQueryRepository entityQueryRepository;
private final DbTypeInfoComponent dbTypeInfoComponent;
private final TbApiUsageReportClient apiUsageReportClient;
private final NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Lazy
private TelemetrySubscriptionService tsSubService;
@ -251,8 +254,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
state.setLastActivityTime(lastReportedActivity);
if (!state.isActive()) {
state.setActive(true);
save(deviceId, ACTIVITY_STATE, true);
pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
onDeviceActivityStatusChange(deviceId, true, stateData);
}
} else {
log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity);
@ -488,9 +490,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
if (partitionService.resolve(ServiceType.TB_CORE, stateData.getTenantId(), deviceId).isMyPartition()) {
state.setActive(false);
state.setLastInactivityAlarmTime(ts);
save(deviceId, ACTIVITY_STATE, false);
onDeviceActivityStatusChange(deviceId, false, stateData);
save(deviceId, INACTIVITY_ALARM_TIME, ts);
pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
} else {
cleanupEntity(deviceId);
}
@ -530,6 +531,19 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
private void onDeviceActivityStatusChange(DeviceId deviceId, boolean active, DeviceStateData stateData) {
save(deviceId, ACTIVITY_STATE, active);
pushRuleEngineMessage(stateData, active ? ACTIVITY_EVENT : INACTIVITY_EVENT);
TbMsgMetaData metaData = stateData.getMetaData();
notificationRuleProcessor.process(DeviceActivityTrigger.builder()
.tenantId(stateData.getTenantId()).customerId(stateData.getCustomerId())
.deviceId(deviceId).active(active)
.deviceName(metaData.getValue("deviceName"))
.deviceType(metaData.getValue("deviceType"))
.deviceLabel(metaData.getValue("deviceLabel"))
.build());
}
private boolean cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
boolean cleanup = !partitionedEntities.containsKey(tpi);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;

6
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java

@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;

8
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java

@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.notification.NotificationRuleService;

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeDao;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import java.util.Arrays;
@ -52,6 +53,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
private static final LinkedHashSet<EntityType> HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET));
private final TbRuleChainService tbRuleChainService;
private final RuleChainService ruleChainService;
private final RuleNodeDao ruleNodeDao;
@ -106,7 +108,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
ruleChain = ruleChainService.saveRuleChain(ruleChain);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
exportData.getMetaData().setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData());
ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData(), tbRuleChainService::updateRuleNodeConfiguration);
return ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChain.getId());
} else {
return ruleChain;

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

@ -47,12 +47,12 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;

4
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -28,8 +28,8 @@ import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;

3
application/src/main/resources/logback.xml

@ -51,6 +51,9 @@
<!-- MQTT transport debug -->
<!-- <logger name="org.thingsboard.server.transport.mqtt.MqttTransportHandler" level="DEBUG" /> -->
<!-- Device actor message processor debug -->
<!-- <logger name="org.thingsboard.server.actors.device.DeviceActorMessageProcessor" level="DEBUG" />-->
<logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" />
<root level="INFO">

52
application/src/main/resources/templates/mail_config_templates.json

@ -0,0 +1,52 @@
[
{
"providerId": "SENDGRID",
"smtpProtocol": "SMTPS",
"smtpHost": "smtp.sendgrid.net",
"smtpPort": 465,
"timeout": 10000,
"enableTls": true,
"tlsVersion": "TLSv1.2",
"authorizationUri": null,
"accessTokenUri": null,
"scope": [
""
],
"helpLink": null,
"name": "SendGrid"
},
{
"providerId": "GOOGLE",
"smtpProtocol": "SMTPS",
"smtpHost": "smtp.gmail.com",
"smtpPort": 465,
"timeout": 10000,
"enableTls": true,
"tlsVersion": "TLSv1.2",
"authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline",
"accessTokenUri": "https://oauth2.googleapis.com/token",
"scope": [
"https://mail.google.com/"
],
"helpLink": "https://support.google.com/googleapi/answer/6158849",
"name": "Google"
},
{
"providerId": "OFFICE_365",
"smtpProtocol": "SMTP",
"smtpHost": "smtp.office365.com",
"smtpPort": 587,
"timeout": 10000,
"enableTls": true,
"tlsVersion": "TLSv1.2",
"authorizationUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize",
"accessTokenUri": "https://login.microsoftonline.com/%s/oauth2/v2.0/token",
"scope": [
"https://outlook.office365.com/SMTP.Send",
"offline_access",
"openid"
],
"helpLink": "https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth",
"name": "Office 365"
}
]

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

@ -135,6 +135,10 @@ security:
path: "${SECURITY_JAVA_CACERTS_PATH:${java.home}/lib/security/cacerts}"
password: "${SECURITY_JAVA_CACERTS_PASSWORD:changeit}"
mail:
oauth2:
refreshTokenCheckingInterval: "${REFRESH_TOKEN_EXPIRATION_CHECKING_INTERVAL:86400}" # Number of seconds (1 day).
# Usage statistics parameters
usage:
stats:
@ -389,6 +393,10 @@ actors:
queue_size: "${ACTORS_RULE_TRANSACTION_QUEUE_SIZE:15000}"
# Time in milliseconds for transaction to complete
duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}"
external:
# Force acknowledgement of the incoming message for external rule nodes to decrease processing latency.
# Enqueue the result of external node processing as a separate message to the rule engine.
force_ack: "${ACTORS_RULE_EXTERNAL_NODE_FORCE_ACK:false}"
rpc:
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
sequential: "${ACTORS_RPC_SEQUENTIAL:false}"
@ -502,7 +510,7 @@ cache:
spring.data.redis.repositories.enabled: false
redis:
# standalone or cluster
# standalone or cluster or sentinel
connection:
type: "${REDIS_CONNECTION_TYPE:standalone}"
standalone:
@ -522,6 +530,16 @@ redis:
nodes: "${REDIS_NODES:}"
# Maximum number of redirects to follow when executing commands across the cluster.
max-redirects: "${REDIS_MAX_REDIRECTS:12}"
# if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
sentinel:
# name of master node
master: "${REDIS_MASTER:}"
# comma-separated list of "host:port" pairs of sentinels
sentinels: "${REDIS_SENTINELS:}"
# password to authenticate with sentinel
password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
db: "${REDIS_DB:0}"
@ -1263,6 +1281,9 @@ vc:
notification_system:
thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}"
rules:
# Semicolon-separated deduplication durations (in millis) for trigger types. Format: 'NotificationRuleTriggerType1:123;NotificationRuleTriggerType2:456'
deduplication_durations: "${TB_NOTIFICATION_RULES_DEDUPLICATION_DURATIONS:NEW_PLATFORM_VERSION:0;RATE_LIMITS:14400000;}"
management:
endpoints:

20
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -264,7 +264,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
int cntTime = 1;
testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionType, cntTime);
testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo);
tesPushMsgToCoreTime(cntTime);
testPushMsgToCoreTime(cntTime);
Mockito.reset(tbClusterService, auditLogService);
}
@ -363,13 +363,13 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
Mockito.any(entityId.getClass()), Mockito.any(ComponentLifecycleEvent.class));
}
private void tesPushMsgToCoreTime(int cntTime) {
private void testPushMsgToCoreTime(int cntTime) {
Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull());
}
protected void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId,
CustomerId customerId, UserId userId, String userName,
ActionType actionType, int cntTime, Object... additionalInfo) {
CustomerId customerId, UserId userId, String userName,
ActionType actionType, int cntTime, Object... additionalInfo) {
ArgumentMatcher<HasName> matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString());
ArgumentMatcher<EntityId> matcherOriginatorId = argument -> argument.equals(originatorId);
ArgumentMatcher<CustomerId> matcherCustomerId = customerId == null ?
@ -380,10 +380,10 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
actionType, cntTime, extractMatcherAdditionalInfo(additionalInfo));
}
private void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId,
CustomerId customerId, UserId userId, String userName,
ActionType actionType, int cntTime, Object... additionalInfo) {
ArgumentMatcher<HasName> matcherEntityEquals = argument -> argument.getClass().equals(entity.getClass());
protected void testLogEntityActionEntityEqClass(HasName entity, EntityId originatorId, TenantId tenantId,
CustomerId customerId, UserId userId, String userName,
ActionType actionType, int cntTime, Object... additionalInfo) {
ArgumentMatcher<HasName> matcherEntityEquals = argument -> entity.getClass().isAssignableFrom(argument.getClass());
ArgumentMatcher<EntityId> matcherOriginatorId = argument -> argument.equals(originatorId);
ArgumentMatcher<CustomerId> matcherCustomerId = customerId == null ?
argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId);
@ -600,8 +600,8 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
return fieldName + " length must be equal or less than 255";
}
protected String msgErrorNoFound(String entityClassName, String assetIdStr) {
return entityClassName + " with id [" + assetIdStr + "] is not found";
protected String msgErrorNoFound(String entityClassName, String entityIdStr) {
return entityClassName + " with id [" + entityIdStr + "] is not found";
}
private String entityClassToEntityTypeName(HasName entity) {

45
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -168,6 +168,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
private static final String CUSTOMER_USER_PASSWORD = "customer";
protected static final String DIFFERENT_CUSTOMER_USER_EMAIL = "testdifferentcustomer@thingsboard.org";
protected static final String DIFFERENT_TENANT_CUSTOMER_USER_EMAIL = "testdifferenttenantcustomer@thingsboard.org";
private static final String DIFFERENT_CUSTOMER_USER_PASSWORD = "diffcustomer";
/**
@ -193,9 +195,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected CustomerId customerId;
protected TenantId differentTenantId;
protected CustomerId differentCustomerId;
protected CustomerId differentTenantCustomerId;
protected UserId customerUserId;
protected UserId differentCustomerUserId;
protected UserId differentTenantCustomerUserId;
@SuppressWarnings("rawtypes")
private HttpMessageConverter mappingJackson2HttpMessageConverter;
@ -367,7 +373,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected Tenant savedDifferentTenant;
protected User savedDifferentTenantUser;
private Customer savedDifferentCustomer;
private Customer savedDifferentTenantCustomer;
protected User differentCustomerUser;
protected User differentTenantCustomerUser;
protected void loginDifferentTenant() throws Exception {
if (savedDifferentTenant != null) {
@ -409,6 +417,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
}
protected void loginDifferentTenantCustomer() throws Exception {
if (savedDifferentTenantCustomer != null) {
login(savedDifferentTenantCustomer.getEmail(), CUSTOMER_USER_PASSWORD);
} else {
createDifferentTenantCustomer();
loginDifferentTenant();
differentTenantCustomerUser = new User();
differentTenantCustomerUser.setAuthority(Authority.CUSTOMER_USER);
differentTenantCustomerUser.setTenantId(savedDifferentTenantCustomer.getTenantId());
differentTenantCustomerUser.setCustomerId(savedDifferentTenantCustomer.getId());
differentTenantCustomerUser.setEmail(DIFFERENT_TENANT_CUSTOMER_USER_EMAIL);
differentTenantCustomerUser = createUserAndLogin(differentTenantCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD);
differentTenantCustomerUserId = differentTenantCustomerUser.getId();
}
}
protected void createDifferentCustomer() throws Exception {
loginTenantAdmin();
@ -421,6 +447,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
resetTokens();
}
protected void createDifferentTenantCustomer() throws Exception {
loginDifferentTenant();
Customer customer = new Customer();
customer.setTitle("Different tenant customer");
savedDifferentTenantCustomer = doPost("/api/customer", customer, Customer.class);
Assert.assertNotNull(savedDifferentTenantCustomer);
differentTenantCustomerId = savedDifferentTenantCustomer.getId();
resetTokens();
}
protected void deleteDifferentTenant() throws Exception {
if (savedDifferentTenant != null) {
loginSysAdmin();
@ -603,6 +641,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(getRequest);
}
protected ResultActions doGet(String urlTemplate, HttpHeaders httpHeaders, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
getRequest.headers(httpHeaders);
setJwtToken(getRequest);
return mockMvc.perform(getRequest);
}
protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
}

23
application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java

@ -46,6 +46,7 @@ import java.util.LinkedList;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@ -104,7 +105,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
AlarmComment createdComment = createAlarmComment(alarm.getId());
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
}
@Test
@ -116,7 +117,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
AlarmComment createdComment = createAlarmComment(alarm.getId());
Assert.assertEquals(AlarmCommentType.OTHER, createdComment.getType());
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment);
}
@Test
@ -135,7 +136,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText());
Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn"));
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment);
}
@Test
@ -154,7 +155,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText());
Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn"));
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment);
}
@Test
@ -169,8 +170,8 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
savedComment.setComment(newComment);
doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment)
.andExpect(status().isForbidden())
.andExpect(statusReason(containsString(msgErrorPermission)));
.andExpect(status().isNotFound())
.andExpect(statusReason(equalTo(msgErrorNoFound("Alarm", alarm.getId().toString()))));
testNotifyEntityNever(alarm.getId(), savedComment);
}
@ -209,7 +210,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
.comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment",
CUSTOMER_USER_EMAIL)))
.build();
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
}
@Test
@ -228,7 +229,7 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
.comment(JacksonUtil.newObjectNode().put("text", String.format("User %s deleted his comment",
TENANT_ADMIN_EMAIL)))
.build();
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
testLogEntityActionEntityEqClass(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, expectedAlarmComment);
}
@Test
@ -355,16 +356,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
AlarmComment alarmComment = AlarmComment.builder()
.comment(JacksonUtil.newObjectNode().set("text", new TextNode(text)))
.build();
return saveAlarmComment(alarmId, alarmComment);
}
private AlarmComment createAlarmComment(AlarmId alarmId) {
private AlarmComment createAlarmComment(AlarmId alarmId) {
return createAlarmComment(alarmId, "Please take a look");
}
private AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) {
alarmComment = doPost("/api/alarm/" + alarmId + "/comment", alarmComment, AlarmComment.class);
Assert.assertNotNull(alarmComment);

76
application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
@ -30,6 +31,9 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbCreateAlarmNode;
import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -129,6 +133,78 @@ public class RuleChainControllerTest extends AbstractControllerTest {
ActionType.UPDATED);
}
@Test
public void testSaveRuleChainMetadataWithVersionedNodes() throws Exception {
RuleChain ruleChain = new RuleChain();
ruleChain.setName("RuleChain");
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
Assert.assertNotNull(savedRuleChain);
RuleChainId ruleChainId = savedRuleChain.getId();
Assert.assertNotNull(ruleChainId);
Assert.assertTrue(savedRuleChain.getCreatedTime() > 0);
Assert.assertEquals(ruleChain.getName(), savedRuleChain.getName());
var annotation = TbGetRelatedAttributeNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class);
String ruleNodeType = TbGetRelatedAttributeNode.class.getName();
int currentVersion = annotation.version();
String oldConfig = "{\"attrMapping\":{\"serialNumber\":\"sn\"}," +
"\"relationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1," +
"\"filters\":[{\"relationType\":\"Contains\",\"entityTypes\":[]}]," +
"\"fetchLastLevelOnly\":false},\"telemetry\":false}";
TbGetRelatedDataNodeConfiguration defaultConfiguration = new TbGetRelatedDataNodeConfiguration().defaultConfiguration();
String newConfig = JacksonUtil.toString(defaultConfiguration);
var ruleChainMetaData = createRuleChainMetadataWithTbVersionedNodes(
ruleChainId,
ruleNodeType,
currentVersion,
oldConfig,
newConfig
);
var savedRuleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
Assert.assertEquals(ruleChainId, savedRuleChainMetaData.getRuleChainId());
Assert.assertEquals(2, savedRuleChainMetaData.getNodes().size());
for (RuleNode ruleNode : savedRuleChainMetaData.getNodes()) {
Assert.assertNotNull(ruleNode.getId());
Assert.assertEquals(currentVersion, ruleNode.getConfigurationVersion());
Assert.assertEquals(defaultConfiguration, JacksonUtil.treeToValue(ruleNode.getConfiguration(), defaultConfiguration.getClass()));
}
}
private RuleChainMetaData createRuleChainMetadataWithTbVersionedNodes(
RuleChainId ruleChainId,
String ruleNodeType,
int currentVersion,
String oldConfig,
String newConfig
) {
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
ruleChainMetaData.setRuleChainId(ruleChainId);
var ruleNodeWithOldConfig = new RuleNode();
ruleNodeWithOldConfig.setName("Old Rule Node");
ruleNodeWithOldConfig.setType(ruleNodeType);
ruleNodeWithOldConfig.setConfiguration(JacksonUtil.toJsonNode(oldConfig));
var ruleNodeWithNewConfig = new RuleNode();
ruleNodeWithNewConfig.setName("New Rule Node");
ruleNodeWithNewConfig.setType(ruleNodeType);
ruleNodeWithNewConfig.setConfigurationVersion(currentVersion);
ruleNodeWithNewConfig.setConfiguration(JacksonUtil.toJsonNode(newConfig));
List<RuleNode> ruleNodes = new ArrayList<>();
ruleNodes.add(ruleNodeWithOldConfig);
ruleNodes.add(ruleNodeWithNewConfig);
ruleChainMetaData.setFirstNodeIndex(0);
ruleChainMetaData.setNodes(ruleNodes);
return ruleChainMetaData;
}
@Test
public void testSaveRuleChainWithViolationOfLengthValidation() throws Exception {

251
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

File diff suppressed because one or more lines are too long

9
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -100,6 +100,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@TestPropertySource(properties = {
"edges.enabled=true",
"queue.rule-engine.stats.enabled=false",
})
abstract public class AbstractEdgeTest extends AbstractControllerTest {
@ -181,14 +182,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@After
public void afterTest() throws Exception {
try {
edgeImitator.disconnect();
} catch (Exception ignored){}
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getUuidId())
.andExpect(status().isOk());
try {
edgeImitator.disconnect();
} catch (Exception ignored) {}
}
private void installation() {

4
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -94,8 +94,6 @@ public class EdgeImitator {
@Getter
private UplinkResponseMsg latestResponseMsg;
private boolean connected = false;
public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException {
edgeRpcClient = new EdgeGrpcClient();
messagesLatch = new CountDownLatch(0);
@ -120,7 +118,6 @@ public class EdgeImitator {
}
public void connect() {
connected = true;
edgeRpcClient.connect(routingKey, routingSecret,
this::onUplinkResponse,
this::onEdgeUpdate,
@ -131,7 +128,6 @@ public class EdgeImitator {
}
public void disconnect() throws InterruptedException {
connected = false;
edgeRpcClient.disconnect(false);
}

10
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java

@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.metadata.FetchTo;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -137,16 +139,20 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
@ -239,8 +245,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -271,8 +279,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

4
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -24,6 +24,8 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.FetchTo;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -92,8 +94,10 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
RuleNode ruleNode = new RuleNode();
ruleNode.setName("Simple Rule Node");
ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setFetchTo(FetchTo.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
ruleNode.setConfiguration(JacksonUtil.valueToTree(configuration));

10
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -24,11 +24,12 @@ import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.util.limits.DefaultRateLimitService;
import org.thingsboard.server.dao.util.limits.LimitedApi;
import org.thingsboard.server.dao.util.limits.RateLimitService;
import java.util.List;
@ -37,6 +38,7 @@ import java.util.UUID;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
@ -50,7 +52,7 @@ public class RateLimitServiceTest {
@Before
public void beforeEach() {
tenantProfileCache = Mockito.mock(TbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, 60, 100);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID());
}
@ -73,14 +75,14 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_EXPORT,
LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES
)) {
testRateLimits(limitedApi, max, tenantId);
}
CustomerId customerId = new CustomerId(UUID.randomUUID());
testRateLimits(LimitedApi.REST_REQUESTS, max, customerId);
testRateLimits(LimitedApi.REST_REQUESTS_PER_CUSTOMER, max, customerId);
NotificationRuleId notificationRuleId = new NotificationRuleId(UUID.randomUUID());
testRateLimits(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, max, notificationRuleId);

40
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.util.Pair;
@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification;
@ -38,11 +40,12 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -54,6 +57,10 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import java.net.URISyntaxException;
import java.util.Arrays;
@ -72,21 +79,40 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
@MockBean
protected SlackService slackService;
@Autowired
protected MailService mailService;
@Autowired
protected NotificationRuleService notificationRuleService;
@Autowired
protected NotificationTemplateService notificationTemplateService;
@Autowired
protected NotificationTargetService notificationTargetService;
@Autowired
protected NotificationRequestService notificationRequestService;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@After
public void afterEach() {
notificationRequestService.deleteNotificationRequestsByTenantId(TenantId.SYS_TENANT_ID);
notificationRuleService.deleteNotificationRulesByTenantId(TenantId.SYS_TENANT_ID);
notificationTemplateService.deleteNotificationTemplatesByTenantId(TenantId.SYS_TENANT_ID);
notificationTargetService.deleteNotificationTargetsByTenantId(TenantId.SYS_TENANT_ID);
}
protected NotificationTarget createNotificationTarget(UserId... usersIds) {
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setTenantId(tenantId);
notificationTarget.setName("Users " + List.of(usersIds));
PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
UserListFilter filter = new UserListFilter();
filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds)));
targetConfig.setUsersFilter(filter);
return createNotificationTarget(filter);
}
protected NotificationTarget createNotificationTarget(UsersFilter usersFilter) {
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setName(usersFilter.toString());
PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig();
targetConfig.setUsersFilter(usersFilter);
notificationTarget.setConfiguration(targetConfig);
return saveNotificationTarget(notificationTarget);
}

2
application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java

@ -26,7 +26,7 @@ import org.thingsboard.server.dao.settings.AdminSettingsService;
public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, null, null);
super(adminSettingsService, null, null, null);
}
@Override

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

Loading…
Cancel
Save