Browse Source

Merge branch 'develop/3.5' of github.com:AndriiLandiak/thingsboard into feature/x509-device-cert-impr

pull/7935/head
Andrii Landiak 3 years ago
parent
commit
1674223e63
  1. 4
      application/pom.xml
  2. 271
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  3. 40
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 41
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
  6. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  7. 67
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
  8. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
  9. 7
      application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
  10. 4
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  11. 2
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  12. 2
      application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java
  13. 85
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  14. 3
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  15. 345
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  16. 2
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  17. 320
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  18. 103
      application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
  19. 199
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  20. 151
      application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
  21. 2
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  22. 4
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  23. 15
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  24. 5
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  25. 75
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  26. 2
      application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java
  27. 2
      application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java
  28. 2
      application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java
  29. 2
      application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java
  30. 2
      application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java
  31. 2
      application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java
  32. 2
      application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java
  33. 8
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  34. 12
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  35. 26
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  36. 28
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java
  37. 36
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java
  38. 6
      application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java
  39. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  40. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  41. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  42. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java
  43. 11
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
  44. 46
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  45. 33
      application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java
  46. 35
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  47. 3
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  48. 2
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  49. 431
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  50. 176
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java
  51. 163
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  52. 25
      application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java
  53. 48
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  54. 30
      application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
  55. 50
      application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
  56. 55
      application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
  57. 201
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java
  58. 82
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  59. 90
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  60. 120
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  61. 76
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java
  62. 60
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
  63. 89
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  64. 56
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
  65. 35
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
  66. 98
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
  67. 27
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java
  68. 5
      application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java
  69. 66
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  70. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  71. 7
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  72. 2
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  73. 8
      application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java
  74. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java
  75. 20
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  76. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  77. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  78. 153
      application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
  79. 12
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  80. 93
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  81. 45
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  82. 31
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  83. 2
      application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java
  84. 2
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java
  85. 6
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  86. 14
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  87. 19
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
  88. 16
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  89. 9
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java
  90. 6
      application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java
  91. 10
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java
  92. 18
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  93. 17
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
  94. 7
      application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java
  95. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
  96. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
  97. 92
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
  98. 6
      application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
  99. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  100. 2
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

4
application/pom.xml

@ -350,6 +350,10 @@
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-api-client</artifactId>
</dependency>
</dependencies>
<build>

271
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -87,14 +87,91 @@ CREATE TABLE IF NOT EXISTS alarm_comment (
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id);
-- ALARM COMMENTS END
-- NOTIFICATIONS START
CREATE TABLE IF NOT EXISTS notification_target (
id UUID NOT NULL CONSTRAINT notification_target_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_target_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_target_tenant_id_created_time ON notification_target(tenant_id, created_time DESC);
CREATE TABLE IF NOT EXISTS notification_template (
id UUID NOT NULL CONSTRAINT notification_template_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
notification_type VARCHAR(50) NOT NULL,
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_template_tenant_id_created_time ON notification_template(tenant_id, created_time DESC);
CREATE TABLE IF NOT EXISTS notification_rule (
id UUID NOT NULL CONSTRAINT notification_rule_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
template_id UUID NOT NULL CONSTRAINT fk_notification_rule_template_id REFERENCES notification_template(id),
trigger_type VARCHAR(50) NOT NULL,
trigger_config VARCHAR(1000) NOT NULL,
recipients_config VARCHAR(10000) NOT NULL,
additional_config VARCHAR(255),
CONSTRAINT uq_notification_rule_name UNIQUE (tenant_id, name)
);
CREATE INDEX IF NOT EXISTS idx_notification_rule_tenant_id_trigger_type_created_time ON notification_rule(tenant_id, trigger_type, created_time DESC);
CREATE TABLE IF NOT EXISTS notification_request (
id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
targets VARCHAR(10000) NOT NULL,
template_id UUID,
template VARCHAR(10000),
info VARCHAR(1000),
additional_config VARCHAR(1000),
originator_entity_id UUID,
originator_entity_type VARCHAR(32),
rule_id UUID NULL,
status VARCHAR(32),
stats VARCHAR(10000)
);
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_user_created_time ON notification_request(tenant_id, created_time DESC)
WHERE originator_entity_type = 'USER';
CREATE INDEX IF NOT EXISTS idx_notification_request_rule_id_originator_entity_id ON notification_request(rule_id, originator_entity_id)
WHERE originator_entity_type = 'ALARM';
CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_request(status)
WHERE status = 'SCHEDULED';
CREATE TABLE IF NOT EXISTS notification (
id UUID NOT NULL,
created_time BIGINT NOT NULL,
request_id UUID NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL,
subject VARCHAR(255),
body VARCHAR(1000) NOT NULL,
additional_config VARCHAR(1000),
status VARCHAR(32)
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC);
-- NOTIFICATIONS END
ALTER TABLE tb_user ADD COLUMN IF NOT EXISTS phone VARCHAR(255);
CREATE TABLE IF NOT EXISTS user_settings (
user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY,
settings varchar(100000),
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE
);
-- ALARM COMMENTS END
-- ALARM INFO VIEW
DROP VIEW IF EXISTS alarm_info CASCADE;
@ -268,7 +345,7 @@ BEGIN
UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id;
END IF;
SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text;
END
$$;
@ -355,3 +432,191 @@ ALTER TABLE device_profile
ADD CONSTRAINT device_profile_credentials_hash_unq_key UNIQUE (certificate_hash);
-- DEVICE PROFILE CERTIFICATE END
-- TTL DROP PARTITIONS FUNCTIONS UPDATE START
DROP PROCEDURE IF EXISTS drop_partitions_by_max_ttl(character varying, bigint, bigint);
DROP FUNCTION IF EXISTS get_partition_by_max_ttl_date;
CREATE OR REPLACE FUNCTION get_partition_by_system_ttl_date(IN partition_type varchar, IN date timestamp, OUT partition varchar) AS
$$
BEGIN
CASE
WHEN partition_type = 'DAYS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM') || '_' || to_char(date, 'dd');
WHEN partition_type = 'MONTHS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
ELSE
partition := NULL;
END CASE;
IF partition IS NOT NULL THEN
IF NOT EXISTS(SELECT
FROM pg_tables
WHERE schemaname = 'public'
AND tablename = partition) THEN
partition := NULL;
RAISE NOTICE 'Failed to found partition by ttl';
END IF;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE PROCEDURE drop_partitions_by_system_ttl(IN partition_type varchar, IN system_ttl bigint, INOUT deleted bigint)
LANGUAGE plpgsql AS
$$
DECLARE
date timestamp;
partition_by_max_ttl_date varchar;
partition_by_max_ttl_month varchar;
partition_by_max_ttl_day varchar;
partition_by_max_ttl_year varchar;
partition varchar;
partition_year integer;
partition_month integer;
partition_day integer;
BEGIN
if system_ttl IS NOT NULL AND system_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - system_ttl);
partition_by_max_ttl_date := get_partition_by_system_ttl_date(partition_type, date);
RAISE NOTICE 'Date by max ttl: %', date;
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_by_max_ttl_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
IF partition_by_max_ttl_year IS NULL THEN
RAISE NOTICE 'Failed to remove partitions by max ttl date due to partition_by_max_ttl_year is null!';
ELSE
IF partition_type = 'YEARS' THEN
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END LOOP;
ELSE
IF partition_type = 'MONTHS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove months partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_year = partition_by_max_ttl_year::integer THEN
IF partition_month >= partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
ELSE
IF partition_type = 'DAYS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
IF partition_by_max_ttl_day IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_day is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_month > partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_month < partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_day := SPLIT_PART(partition, '_', 5)::integer;
IF partition_day >= partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_day < partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END
$$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END

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

@ -30,7 +30,9 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
@ -68,6 +70,11 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleProcessingService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -90,6 +97,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -307,6 +315,10 @@ public class ActorSystemContext {
@Getter
private ExternalCallExecutorService externalCallExecutorService;
@Autowired
@Getter
private NotificationExecutorService notificationExecutor;
@Autowired
@Getter
private SharedEventLoopGroupService sharedEventLoopGroupService;
@ -323,6 +335,34 @@ public class ActorSystemContext {
@Getter
private SmsSenderFactory smsSenderFactory;
@Autowired
@Getter
private NotificationCenter notificationCenter;
@Autowired
@Getter
private NotificationRuleProcessingService notificationRuleProcessingService;
@Autowired
@Getter
private NotificationTargetService notificationTargetService;
@Autowired
@Getter
private NotificationTemplateService notificationTemplateService;
@Autowired
@Getter
private NotificationRequestService notificationRequestService;
@Autowired
@Getter
private NotificationRuleService notificationRuleService;
@Autowired
@Getter
private SlackService slackService;
@Lazy
@Autowired(required = false)
@Getter

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

@ -25,6 +25,7 @@ import org.bouncycastle.util.Arrays;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -35,6 +36,7 @@ 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.TbRelationTypes;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.rule.engine.util.TenantIdLoader;
import org.thingsboard.server.actors.ActorSystemContext;
@ -86,6 +88,10 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
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 org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -472,6 +478,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getExternalCallExecutorService();
}
@Override
public ListeningExecutor getNotificationExecutor() {
return mainCtx.getNotificationExecutor();
}
@Override
@Deprecated
public ScriptEngine createJsScriptEngine(String script, String... argNames) {
@ -686,6 +697,36 @@ class DefaultTbContext implements TbContext {
return mainCtx.getSmsSenderFactory();
}
@Override
public NotificationCenter getNotificationCenter() {
return mainCtx.getNotificationCenter();
}
@Override
public NotificationTargetService getNotificationTargetService() {
return mainCtx.getNotificationTargetService();
}
@Override
public NotificationTemplateService getNotificationTemplateService() {
return mainCtx.getNotificationTemplateService();
}
@Override
public NotificationRequestService getNotificationRequestService() {
return mainCtx.getNotificationRequestService();
}
@Override
public NotificationRuleService getNotificationRuleService() {
return mainCtx.getNotificationRuleService();
}
@Override
public SlackService getSlackService() {
return mainCtx.getSlackService();
}
@Override
public RuleEngineRpcService getRpcService() {
return mainCtx.getTbRuleEngineDeviceRpcService();

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java

@ -20,7 +20,6 @@ import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
@ -30,7 +29,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
public class RuleChainActor extends RuleEngineComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
private final RuleChain ruleChain;
@ -101,6 +100,16 @@ public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMe
}
}
@Override
protected RuleChainId getRuleChainId() {
return ruleChain.getId();
}
@Override
protected String getRuleChainName() {
return ruleChain.getName();
}
@Override
protected long getErrorPersistFrequency() {
return systemContext.getRuleChainErrorPersistFrequency();

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

@ -94,7 +94,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
@Override
public String getComponentName() {
return null;
return ruleChainName;
}
@Override

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

@ -0,0 +1,67 @@
/**
* 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.ruleChain;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbRuleNodeUpdateException;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.id.EntityId;
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.dao.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
public abstract class RuleEngineComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ComponentActor<T, P> {
public RuleEngineComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) {
super(systemContext, tenantId, id);
}
@Override
protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
super.logLifecycleEvent(event, e);
if (e instanceof TbRuleNodeUpdateException || (event == ComponentLifecycleEvent.STARTED && e != null)) {
return;
}
processNotificationRule(event, e);
}
@Override
public void destroy(TbActorStopReason stopReason, Throwable cause) {
super.destroy(stopReason, cause);
if (stopReason == TbActorStopReason.INIT_FAILED && cause != null) {
processNotificationRule(ComponentLifecycleEvent.STARTED, cause);
}
}
private void processNotificationRule(ComponentLifecycleEvent event, Throwable e) {
systemContext.getNotificationRuleProcessingService().process(tenantId, RuleEngineComponentLifecycleEventTrigger.builder()
.ruleChainId(getRuleChainId())
.ruleChainName(getRuleChainName())
.componentId(id)
.componentName(processor.getComponentName())
.eventType(event)
.error(e)
.build());
}
protected abstract RuleChainId getRuleChainId();
protected abstract String getRuleChainName();
}

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java

@ -21,7 +21,6 @@ import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -32,7 +31,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
@Slf4j
public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
private final String ruleChainName;
private final RuleChainId ruleChainId;
@ -133,6 +132,16 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
}
}
@Override
protected RuleChainId getRuleChainId() {
return ruleChainId;
}
@Override
protected String getRuleChainName() {
return ruleChainName;
}
@Override
protected long getErrorPersistFrequency() {
return systemContext.getRuleNodeErrorPersistFrequency();

7
application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java

@ -17,7 +17,6 @@ package org.thingsboard.server.actors.service;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorException;
import org.thingsboard.server.actors.TbRuleNodeUpdateException;
@ -26,6 +25,7 @@ import org.thingsboard.server.actors.stats.StatsPersistMsg;
import org.thingsboard.server.common.data.id.EntityId;
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.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
@ -83,7 +83,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
}
@Override
public void destroy() {
public void destroy(TbActorStopReason stopReason, Throwable cause) {
try {
log.debug("[{}][{}][{}] Stopping processor.", tenantId, id, id.getEntityType());
if (processor != null) {
@ -181,9 +181,10 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
logLifecycleEvent(event, null);
}
private void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
systemContext.persistLifecycleEvent(tenantId, id, event, e);
}
protected abstract long getErrorPersistFrequency();
}

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

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
@ -105,7 +106,7 @@ public class TenantActor extends RuleChainManagerActor {
}
@Override
public void destroy() {
public void destroy(TbActorStopReason stopReason, Throwable cause) {
log.info("[{}] Stopping tenant actor.", tenantId);
}
@ -208,6 +209,7 @@ public class TenantActor extends RuleChainManagerActor {
log.trace("[{}] Ack message because Rule Engine is disabled", tenantId);
tbMsg.getCallback().onSuccess();
}
systemContext.getNotificationRuleProcessingService().process(tenantId, tbMsg);
}
private void onRuleChainMsg(RuleChainAwareMsg msg) {

2
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -25,6 +25,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.security.Authority;
@ -138,6 +139,7 @@ public class SwaggerConfiguration {
)
.securitySchemes(newArrayList(httpLogin()))
.securityContexts(newArrayList(securityContext()))
.ignoredParameterTypes(AuthenticationPrincipal.class)
.enableUrlTemplating(true);
}

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

@ -43,7 +43,7 @@ import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
import org.thingsboard.server.exception.ToErrorResponseEntity;
import javax.annotation.Nullable;
import java.util.Optional;

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

@ -21,29 +21,41 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.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.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
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.exception.ThingsboardException;
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;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
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;
@ -51,43 +63,30 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.system.SystemInfoService;
import org.thingsboard.server.service.update.UpdateService;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController extends BaseController {
@Autowired
private MailService mailService;
@Autowired
private SmsService smsService;
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private SystemSecurityService systemSecurityService;
private final MailService mailService;
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
private final JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired
private EntitiesVersionControlService versionControlService;
@Autowired
private TbAutoCommitSettingsService autoCommitSettingsService;
@Autowired
private UpdateService updateService;
private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService;
private final TbAutoCommitSettingsService autoCommitSettingsService;
private final UpdateService updateService;
private final SystemInfoService systemInfoService;
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@ -109,7 +108,6 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
@ -318,7 +316,6 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -399,4 +396,24 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get system info (getSystemInfo)",
notes = "Get main information about system. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/systemInfo", method = RequestMethod.GET)
@ResponseBody
public SystemInfo getSystemInfo() throws ThingsboardException {
return systemInfoService.getSystemInfo();
}
@ApiOperation(value = "Get features info (getFeaturesInfo)",
notes = "Get information about enabled/disabled features. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/featuresInfo", method = RequestMethod.GET)
@ResponseBody
public FeaturesInfo getFeaturesInfo() {
return systemInfoService.getFeaturesInfo();
}
}

3
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -139,6 +139,9 @@ public class AlarmController extends BaseController {
checkNotNull(alarm.getOriginator());
checkEntity(alarm.getId(), alarm, Resource.ALARM);
checkEntityId(alarm.getOriginator(), Operation.READ);
if (alarm.getAssigneeId() != null) {
checkUserId(alarm.getAssigneeId(), Operation.READ);
}
return tbAlarmService.save(alarm, getCurrentUser());
}

345
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -25,7 +25,6 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@ -43,6 +42,7 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
@ -59,6 +59,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
@ -77,6 +78,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RpcId;
@ -85,6 +87,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
@ -100,6 +103,7 @@ import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
@ -137,6 +141,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.instructions.EdgeInstallService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
@ -150,6 +155,7 @@ import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@ -160,11 +166,12 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isNotEmpty;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID;
import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -302,12 +309,18 @@ public abstract class BaseController {
@Autowired
protected TbNotificationEntityService notificationEntityService;
@Autowired
protected EntityActionService entityActionService;
@Autowired
protected QueueService queueService;
@Autowired
protected EntitiesVersionControlService vcService;
@Autowired
protected ExportableEntitiesService entitiesService;
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@ -380,11 +393,17 @@ public abstract class BaseController {
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
String errorMessage = "Validation error: " + e.getFieldErrors().stream()
.map(fieldError -> {
String property = fieldError.getField();
if (property.equals("valid") || StringUtils.endsWith(property, ".valid")) { // when custom @AssertTrue is used
property = "";
}
return (!property.isEmpty() ? (property + " ") : "") + fieldError.getDefaultMessage();
})
.collect(Collectors.joining(", "));
ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
handleThingsboardException(thingsboardException, response);
handleControllerException(thingsboardException, response);
}
<T> T checkNotNull(T reference) throws ThingsboardException {
@ -470,27 +489,11 @@ public abstract class BaseController {
}
Tenant checkTenantId(TenantId tenantId, Operation operation) throws ThingsboardException {
try {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Tenant tenant = tenantService.findTenantById(tenantId);
checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant);
return tenant;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(tenantId, (t, i) -> tenantService.findTenantById(tenantId), operation);
}
TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException {
try {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
TenantInfo tenant = tenantService.findTenantInfoById(tenantId);
checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant);
return tenant;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(tenantId, (t, i) -> tenantService.findTenantInfoById(tenantId), operation);
}
TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException {
@ -510,33 +513,16 @@ public abstract class BaseController {
}
Customer checkCustomerId(CustomerId customerId, Operation operation) throws ThingsboardException {
try {
validateId(customerId, "Incorrect customerId " + customerId);
Customer customer = customerService.findCustomerById(getTenantId(), customerId);
checkNotNull(customer, "Customer with id [" + customerId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.CUSTOMER, operation, customerId, customer);
return customer;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(customerId, customerService::findCustomerById, operation);
}
User checkUserId(UserId userId, Operation operation) throws ThingsboardException {
try {
validateId(userId, "Incorrect userId " + userId);
User user = userService.findUserById(getCurrentUser().getTenantId(), userId);
checkNotNull(user, "User with id [" + userId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.USER, operation, userId, user);
return user;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(userId, userService::findUserById, operation);
}
protected <I extends EntityId, T extends HasTenantId> void checkEntity(I entityId, T entity, Resource resource) throws ThingsboardException {
if (entityId == null) {
accessControlService
.checkPermission(getCurrentUser(), resource, Operation.CREATE, null, entity);
accessControlService.checkPermission(getCurrentUser(), resource, Operation.CREATE, null, entity);
} else {
checkEntityId(entityId, Operation.WRITE);
}
@ -607,130 +593,69 @@ public abstract class BaseController {
checkQueueId(new QueueId(entityId.getId()), operation);
return;
default:
throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType());
checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}
} catch (Exception e) {
throw handleException(e, false);
}
}
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
protected <E extends HasId<I> & HasTenantId, I extends EntityId> E checkEntityId(I entityId, ThrowingBiFunction<TenantId, I, E> findingFunction, Operation operation) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
Device device = deviceService.findDeviceById(getCurrentUser().getTenantId(), deviceId);
checkNotNull(device, "Device with id [" + deviceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
return device;
validateId((UUIDBased) entityId, "Invalid entity id");
SecurityUser user = getCurrentUser();
E entity = findingFunction.apply(user.getTenantId(), entityId);
checkNotNull(entity, entityId.getEntityType().getNormalName() + " with id [" + entityId + "] is not found");
return checkEntity(user, entity, operation);
} catch (Exception e) {
throw handleException(e, false);
}
}
protected <E extends HasId<I> & HasTenantId, I extends EntityId> E checkEntity(SecurityUser user, E entity, Operation operation) throws ThingsboardException {
checkNotNull(entity, "Entity not found");
accessControlService.checkPermission(user, Resource.of(entity.getId().getEntityType()), operation, entity.getId(), entity);
return entity;
}
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
return checkEntityId(deviceId, deviceService::findDeviceById, operation);
}
DeviceInfo checkDeviceInfoId(DeviceId deviceId, Operation operation) throws ThingsboardException {
try {
validateId(deviceId, "Incorrect deviceId " + deviceId);
DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId);
checkNotNull(device, "Device with id [" + deviceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device);
return device;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(deviceId, deviceService::findDeviceInfoById, operation);
}
DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException {
try {
validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId);
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId);
checkNotNull(deviceProfile, "Device profile with id [" + deviceProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile);
return deviceProfile;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(deviceProfileId, deviceProfileService::findDeviceProfileById, operation);
}
protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
EntityView entityView = entityViewService.findEntityViewById(getCurrentUser().getTenantId(), entityViewId);
checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView);
return entityView;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(entityViewId, entityViewService::findEntityViewById, operation);
}
EntityViewInfo checkEntityViewInfoId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
EntityViewInfo entityView = entityViewService.findEntityViewInfoById(getCurrentUser().getTenantId(), entityViewId);
checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView);
return entityView;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(entityViewId, entityViewService::findEntityViewInfoById, operation);
}
Asset checkAssetId(AssetId assetId, Operation operation) throws ThingsboardException {
try {
validateId(assetId, "Incorrect assetId " + assetId);
Asset asset = assetService.findAssetById(getCurrentUser().getTenantId(), assetId);
checkNotNull(asset, "Asset with id [" + assetId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset);
return asset;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(assetId, assetService::findAssetById, operation);
}
AssetInfo checkAssetInfoId(AssetId assetId, Operation operation) throws ThingsboardException {
try {
validateId(assetId, "Incorrect assetId " + assetId);
AssetInfo asset = assetService.findAssetInfoById(getCurrentUser().getTenantId(), assetId);
checkNotNull(asset, "Asset with id [" + assetId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset);
return asset;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(assetId, assetService::findAssetInfoById, operation);
}
AssetProfile checkAssetProfileId(AssetProfileId assetProfileId, Operation operation) throws ThingsboardException {
try {
validateId(assetProfileId, "Incorrect assetProfileId " + assetProfileId);
AssetProfile assetProfile = assetProfileService.findAssetProfileById(getCurrentUser().getTenantId(), assetProfileId);
checkNotNull(assetProfile, "Asset profile with id [" + assetProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET_PROFILE, operation, assetProfileId, assetProfile);
return assetProfile;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(assetProfileId, assetProfileService::findAssetProfileById, operation);
}
Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);
Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get();
checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarm);
return alarm;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(alarmId, alarmService::findAlarmById, operation);
}
AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);
AlarmInfo alarmInfo = alarmService.findAlarmInfoById(getCurrentUser().getTenantId(), alarmId);
checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo);
return alarmInfo;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(alarmId, alarmService::findAlarmInfoById, operation);
}
AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId, AlarmId alarmId) throws ThingsboardException {
@ -741,82 +666,34 @@ public abstract class BaseController {
if (!alarmId.equals(alarmComment.getAlarmId())) {
throw new ThingsboardException("Alarm id does not match with comment alarm id", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
return alarmComment;
return alarmComment;
} catch (Exception e) {
throw handleException(e, false);
}
}
WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, Operation operation) throws ThingsboardException {
try {
validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(getCurrentUser().getTenantId(), widgetsBundleId);
checkNotNull(widgetsBundle, "Widgets bundle with id [" + widgetsBundleId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, operation, widgetsBundleId, widgetsBundle);
return widgetsBundle;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(widgetsBundleId, widgetsBundleService::findWidgetsBundleById, operation);
}
WidgetTypeDetails checkWidgetTypeId(WidgetTypeId widgetTypeId, Operation operation) throws ThingsboardException {
try {
validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(getCurrentUser().getTenantId(), widgetTypeId);
checkNotNull(widgetTypeDetails, "Widget type with id [" + widgetTypeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetTypeDetails);
return widgetTypeDetails;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(widgetTypeId, widgetTypeService::findWidgetTypeDetailsById, operation);
}
Dashboard checkDashboardId(DashboardId dashboardId, Operation operation) throws ThingsboardException {
try {
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
Dashboard dashboard = dashboardService.findDashboardById(getCurrentUser().getTenantId(), dashboardId);
checkNotNull(dashboard, "Dashboard with id [" + dashboardId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboard);
return dashboard;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(dashboardId, dashboardService::findDashboardById, operation);
}
Edge checkEdgeId(EdgeId edgeId, Operation operation) throws ThingsboardException {
try {
validateId(edgeId, "Incorrect edgeId " + edgeId);
Edge edge = edgeService.findEdgeById(getTenantId(), edgeId);
checkNotNull(edge, "Edge with id [" + edgeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge);
return edge;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(edgeId, edgeService::findEdgeById, operation);
}
EdgeInfo checkEdgeInfoId(EdgeId edgeId, Operation operation) throws ThingsboardException {
try {
validateId(edgeId, "Incorrect edgeId " + edgeId);
EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId);
checkNotNull(edge, "Edge with id [" + edgeId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge);
return edge;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(edgeId, edgeService::findEdgeInfoById, operation);
}
DashboardInfo checkDashboardInfoId(DashboardId dashboardId, Operation operation) throws ThingsboardException {
try {
validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(getCurrentUser().getTenantId(), dashboardId);
checkNotNull(dashboardInfo, "Dashboard with id [" + dashboardId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboardInfo);
return dashboardInfo;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(dashboardId, dashboardService::findDashboardInfoById, operation);
}
ComponentDescriptor checkComponentDescriptorByClazz(String clazz) throws ThingsboardException {
@ -847,11 +724,7 @@ public abstract class BaseController {
}
protected RuleChain checkRuleChain(RuleChainId ruleChainId, Operation operation) throws ThingsboardException {
validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId);
RuleChain ruleChain = ruleChainService.findRuleChainById(getCurrentUser().getTenantId(), ruleChainId);
checkNotNull(ruleChain, "Rule chain with id [" + ruleChainId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.RULE_CHAIN, operation, ruleChainId, ruleChain);
return ruleChain;
return checkEntityId(ruleChainId, ruleChainService::findRuleChainById, operation);
}
protected RuleNode checkRuleNode(RuleNodeId ruleNodeId, Operation operation) throws ThingsboardException {
@ -863,70 +736,27 @@ public abstract class BaseController {
}
TbResource checkResourceId(TbResourceId resourceId, Operation operation) throws ThingsboardException {
try {
validateId(resourceId, "Incorrect resourceId " + resourceId);
TbResource resource = resourceService.findResourceById(getCurrentUser().getTenantId(), resourceId);
checkNotNull(resource, "Resource with id [" + resourceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resource);
return resource;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(resourceId, resourceService::findResourceById, operation);
}
TbResourceInfo checkResourceInfoId(TbResourceId resourceId, Operation operation) throws ThingsboardException {
try {
validateId(resourceId, "Incorrect resourceId " + resourceId);
TbResourceInfo resourceInfo = resourceService.findResourceInfoById(getCurrentUser().getTenantId(), resourceId);
checkNotNull(resourceInfo, "Resource with id [" + resourceId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resourceInfo);
return resourceInfo;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(resourceId, resourceService::findResourceInfoById, operation);
}
OtaPackage checkOtaPackageId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException {
try {
validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId);
OtaPackage otaPackage = otaPackageService.findOtaPackageById(getCurrentUser().getTenantId(), otaPackageId);
checkNotNull(otaPackage, "OTA package with id [" + otaPackageId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackage);
return otaPackage;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(otaPackageId, otaPackageService::findOtaPackageById, operation);
}
OtaPackageInfo checkOtaPackageInfoId(OtaPackageId otaPackageId, Operation operation) throws ThingsboardException {
try {
validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId);
OtaPackageInfo otaPackageIn = otaPackageService.findOtaPackageInfoById(getCurrentUser().getTenantId(), otaPackageId);
checkNotNull(otaPackageIn, "OTA package with id [" + otaPackageId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackageIn);
return otaPackageIn;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(otaPackageId, otaPackageService::findOtaPackageInfoById, operation);
}
Rpc checkRpcId(RpcId rpcId, Operation operation) throws ThingsboardException {
try {
validateId(rpcId, "Incorrect rpcId " + rpcId);
Rpc rpc = rpcService.findById(getCurrentUser().getTenantId(), rpcId);
checkNotNull(rpc, "RPC with id [" + rpcId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.RPC, operation, rpcId, rpc);
return rpc;
} catch (Exception e) {
throw handleException(e, false);
}
return checkEntityId(rpcId, rpcService::findById, operation);
}
protected Queue checkQueueId(QueueId queueId, Operation operation) throws ThingsboardException {
validateId(queueId, "Incorrect queueId " + queueId);
Queue queue = queueService.findQueueById(getCurrentUser().getTenantId(), queueId);
checkNotNull(queue);
accessControlService.checkPermission(getCurrentUser(), Resource.QUEUE, operation, queueId, queue);
Queue queue = checkEntityId(queueId, queueService::findQueueById, operation);
TenantId tenantId = getTenantId();
if (queue.getTenantId().isNullUid() && !tenantId.isNullUid()) {
TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
@ -946,6 +776,40 @@ public abstract class BaseController {
return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
}
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E savedEntity, ActionType actionType) {
logEntityAction(user, entityType, null, savedEntity, actionType, null);
}
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception {
ActionType actionType = entity.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
SecurityUser user = getCurrentUser();
try {
E savedEntity = savingFunction.apply(user.getTenantId(), entity);
logEntityAction(user, entityType, savedEntity, actionType);
return savedEntity;
} catch (Exception e) {
logEntityAction(user, entityType, entity, null, actionType, e);
throw e;
}
}
protected <E extends HasName & HasId<I>, I extends EntityId> void doDeleteAndLog(EntityType entityType, E entity, BiConsumer<TenantId, I> deleteFunction) throws Exception {
SecurityUser user = getCurrentUser();
try {
deleteFunction.accept(user.getTenantId(), entity.getId());
logEntityAction(user, entityType, entity, ActionType.DELETED);
} catch (Exception e) {
logEntityAction(user, entityType, entity, entity, ActionType.DELETED, e);
throw e;
}
}
protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) {
sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action);
}
@ -1003,4 +867,5 @@ public abstract class BaseController {
return null;
}
}
}

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

@ -56,7 +56,7 @@ public class EntityQueryController extends BaseController {
private static final int MAX_PAGE_SIZE = 100;
@ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
@ResponseBody
public long countEntitiesByQuery(

320
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -0,0 +1,320 @@
/**
* 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 io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.info.UserOriginatedNotificationInfo;
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.NotificationTargetType;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationRequestService;
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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import javax.validation.Valid;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class NotificationController extends BaseController {
private final NotificationService notificationService;
private final NotificationRequestService notificationRequestService;
private final NotificationTemplateService notificationTemplateService;
private final NotificationTargetService notificationTargetService;
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "**WebSocket API**:\n\n" +
"There are 2 types of subscriptions: one for unread notifications count, another for unread notifications themselves.\n\n" +
"The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" +
"Subscription command for unread notifications count:\n" +
"```\n{\n \"unreadCountSubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" +
"To subscribe for latest unread notifications:\n" +
"```\n{\n \"unreadSubCmd\": {\n \"cmdId\": 1234,\n \"limit\": 10\n }\n}\n```\n" +
"To unsubscribe from any subscription:\n" +
"```\n{\n \"unsubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" +
"To mark certain notifications as read, use following command:\n" +
"```\n{\n \"markAsReadCmd\": {\n \"cmdId\": 1234,\n \"notifications\": [\n \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\",\n \"5b6dfee0-8d0d-11ed-b61f-35a57b03dade\"\n ]\n }\n}\n\n```\n" +
"To mark all notifications as read:\n" +
"```\n{\n \"markAllAsReadCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" +
"\n\n" +
"Update structure for unread **notifications count subscription**:\n" +
"```\n{\n \"cmdId\": 1234,\n \"totalUnreadCount\": 55\n}\n```\n" +
"For **notifications subscription**:\n" +
"- full update of latest unread notifications:\n" +
"```\n{\n" +
" \"cmdId\": 1234,\n" +
" \"notifications\": [\n" +
" {\n" +
" \"id\": {\n" +
" \"entityType\": \"NOTIFICATION\",\n" +
" \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" +
" },\n" +
" ...\n" +
" }\n" +
" ],\n" +
" \"totalUnreadCount\": 1\n" +
"}\n```\n" +
"- when new notification arrives or shown notification is updated:\n" +
"```\n{\n" +
" \"cmdId\": 1234,\n" +
" \"update\": {\n" +
" \"id\": {\n" +
" \"entityType\": \"NOTIFICATION\",\n" +
" \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" +
" },\n" +
" # updated notification info, text, subject etc.\n" +
" ...\n" +
" },\n" +
" \"totalUnreadCount\": 2\n" +
"}\n```\n" +
"- when unread notifications count changes:\n" +
"```\n{\n" +
" \"cmdId\": 1234,\n" +
" \"totalUnreadCount\": 5\n" +
"}\n```")
@GetMapping("/notifications")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public PageData<Notification> getNotifications(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@RequestParam(defaultValue = "false") boolean unreadOnly,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
}
@PutMapping("/notification/{id}/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markNotificationAsRead(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) {
// no permissions
NotificationId notificationId = new NotificationId(id);
notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId);
}
@PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
// no permissions
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId());
}
@DeleteMapping("/notification/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteNotification(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) {
// no permissions
NotificationId notificationId = new NotificationId(id);
notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId);
}
@PostMapping("/notification/request")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequest createNotificationRequest(@RequestBody @Valid NotificationRequest notificationRequest,
@AuthenticationPrincipal SecurityUser user) throws Exception {
if (notificationRequest.getId() != null) {
throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it");
}
notificationRequest.setTenantId(user.getTenantId());
checkEntity(notificationRequest.getId(), notificationRequest, NOTIFICATION);
notificationRequest.setOriginatorEntityId(user.getId());
if (notificationRequest.getInfo() != null && !(notificationRequest.getInfo() instanceof UserOriginatedNotificationInfo)) {
throw new IllegalArgumentException("Unsupported notification info type");
}
notificationRequest.setRuleId(null);
notificationRequest.setStatus(null);
notificationRequest.setStats(null);
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::processNotificationRequest);
}
@PostMapping("/notification/request/preview")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request,
@RequestParam(defaultValue = "20") int recipientsPreviewSize,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationRequestPreview preview = new NotificationRequestPreview();
request.setOriginatorEntityId(user.getId());
NotificationTemplate template;
if (request.getTemplateId() != null) {
template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ);
} else {
template = request.getTemplate();
}
if (template == null) {
throw new IllegalArgumentException("Template is missing");
}
NotificationProcessingContext tmpProcessingCtx = NotificationProcessingContext.builder()
.tenantId(user.getTenantId())
.request(request)
.settings(null)
.template(template)
.build();
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream()
.collect(Collectors.toMap(m -> m, deliveryMethod -> {
Map<String, String> templateContext;
if (NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods().contains(deliveryMethod)) {
templateContext = tmpProcessingCtx.createTemplateContext(user);
} else {
templateContext = Collections.emptyMap();
}
return tmpProcessingCtx.getProcessedTemplate(deliveryMethod, templateContext);
}));
preview.setProcessedTemplates(processedTemplates);
// generic permission
Set<User> recipientsPreview = new LinkedHashSet<>();
Map<String, Integer> recipientsCountByTarget = new HashMap<>();
List<NotificationTarget> targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(),
request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList()));
for (NotificationTarget target : targets) {
int recipientsCount;
if (target.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), null,
target.getConfiguration(), new PageLink(recipientsPreviewSize));
recipientsCount = (int) recipients.getTotalElements();
for (User recipient : recipients.getData()) {
if (recipientsPreview.size() < recipientsPreviewSize) {
recipientsPreview.add(recipient);
} else {
break;
}
}
} else {
recipientsCount = 1;
}
recipientsCountByTarget.put(target.getName(), recipientsCount);
}
preview.setRecipientsPreview(recipientsPreview);
preview.setRecipientsCountByTarget(recipientsCountByTarget);
preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum());
return preview;
}
@GetMapping("/notification/request/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException {
NotificationRequestId notificationRequestId = new NotificationRequestId(id);
return checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestInfoById, Operation.READ);
}
@GetMapping("/notification/requests")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationRequestInfo> getNotificationRequests(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(user.getTenantId(), EntityType.USER, pageLink);
}
@DeleteMapping("/notification/request/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationRequest(@PathVariable UUID id) throws Exception {
NotificationRequestId notificationRequestId = new NotificationRequestId(id);
NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::deleteNotificationRequest);
}
@PostMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.WRITE);
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId();
notificationSettingsService.saveNotificationSettings(tenantId, notificationSettings);
return notificationSettings;
}
@GetMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ);
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId();
return notificationSettingsService.findNotificationSettings(tenantId);
}
@GetMapping("/notification/deliveryMethods")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ);
return notificationCenter.getAvailableDeliveryMethods(user.getTenantId());
}
}

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

@ -0,0 +1,103 @@
/**
* 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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import javax.validation.Valid;
import java.util.UUID;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@TbCoreComponent
@RequestMapping("/api/notification")
@RequiredArgsConstructor
@Slf4j
public class NotificationRuleController extends BaseController {
private final NotificationRuleService notificationRuleService;
@PostMapping("/rule")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule,
@AuthenticationPrincipal SecurityUser user) throws Exception {
notificationRule.setTenantId(user.getTenantId());
checkEntity(notificationRule.getId(), notificationRule, NOTIFICATION);
NotificationRuleTriggerType triggerType = notificationRule.getTriggerType();
if ((user.isTenantAdmin() && !triggerType.isTenantLevel()) || (user.isSystemAdmin() && triggerType.isTenantLevel())) {
throw new IllegalArgumentException("Trigger type " + triggerType + " is not available");
}
return doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule);
}
@GetMapping("/rule/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException {
NotificationRuleId notificationRuleId = new NotificationRuleId(id);
return checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleInfoById, Operation.READ);
}
@GetMapping("/rules")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationRuleInfo> getNotificationRules(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationRuleService.findNotificationRulesInfosByTenantId(user.getTenantId(), pageLink);
}
@DeleteMapping("/rule/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationRule(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) throws Exception {
NotificationRuleId notificationRuleId = new NotificationRuleId(id);
NotificationRule notificationRule = checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::deleteNotificationRuleById);
tbClusterService.broadcastEntityStateChangeEvent(user.getTenantId(), notificationRuleId, ComponentLifecycleEvent.DELETED);
}
}

199
application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java

@ -0,0 +1,199 @@
/**
* 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 io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@TbCoreComponent
@RequestMapping("/api/notification")
@RequiredArgsConstructor
@Slf4j
public class NotificationTargetController extends BaseController {
private final NotificationTargetService notificationTargetService;
@ApiOperation(value = "Save notification target (saveNotificationTarget)",
notes = "Create or update notification target.\n\n" +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/target")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget,
@AuthenticationPrincipal SecurityUser user) throws Exception {
notificationTarget.setTenantId(user.getTenantId());
checkEntity(notificationTarget.getId(), notificationTarget, NOTIFICATION);
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration();
if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) {
checkTargetUsers(user, targetConfig);
}
return doSaveAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::saveNotificationTarget);
}
@ApiOperation(value = "Get notification target by id (getNotificationTargetById)",
notes = "Fetch saved notification target by id." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget getNotificationTargetById(@PathVariable UUID id) throws ThingsboardException {
NotificationTargetId notificationTargetId = new NotificationTargetId(id);
return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.READ);
}
@ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)",
notes = "Get the list (page) of recipients (users) for such notification target configuration." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/target/recipients")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget,
@RequestParam int pageSize,
@RequestParam int page,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration();
if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) {
checkTargetUsers(user, targetConfig);
} else {
throw new IllegalArgumentException("Target type is not platform users");
}
PageLink pageLink = createPageLink(pageSize, page, null, null, null);
return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), null, notificationTarget.getConfiguration(), pageLink);
}
@GetMapping(value = "/targets", params = {"ids"})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) {
// generic permission
List<NotificationTargetId> targetsIds = Arrays.stream(ids).map(NotificationTargetId::new).collect(Collectors.toList());
return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds);
}
@ApiOperation(value = "Get notification targets (getNotificationTargets)",
notes = "Fetch the page of notification targets owned by sysadmin or tenant." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/targets")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargets(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink);
}
@GetMapping(value = "/targets", params = "notificationType")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) NotificationType notificationType,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink);
}
@ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)",
notes = "Delete notification target by its id.\n\n" +
"This target cannot be referenced by existing scheduled notification requests or any notification rules." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationTargetById(@PathVariable UUID id) throws Exception {
NotificationTargetId notificationTargetId = new NotificationTargetId(id);
NotificationTarget notificationTarget = checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::deleteNotificationTargetById);
}
private void checkTargetUsers(SecurityUser user, NotificationTargetConfig targetConfig) throws ThingsboardException {
if (user.isSystemAdmin()) {
return;
}
// generic permission for users
UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) targetConfig).getUsersFilter();
switch (usersFilter.getType()) {
case USER_LIST:
for (UUID recipientId : ((UserListFilter) usersFilter).getUsersIds()) {
checkUserId(new UserId(recipientId), Operation.READ);
}
break;
case CUSTOMER_USERS:
CustomerId customerId = new CustomerId(((CustomerUsersFilter) usersFilter).getCustomerId());
checkEntityId(customerId, Operation.READ);
break;
case TENANT_ADMINISTRATORS:
if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) ||
CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantProfilesIds())) {
throw new AccessDeniedException("");
}
break;
case SYSTEM_ADMINISTRATORS:
throw new AccessDeniedException("");
}
}
}

151
application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java

@ -0,0 +1,151 @@
/**
* 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 io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import javax.validation.Valid;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api/notification")
public class NotificationTemplateController extends BaseController {
private final NotificationTemplateService notificationTemplateService;
private final NotificationSettingsService notificationSettingsService;
private final SlackService slackService;
@ApiOperation(value = "Save notification template (saveNotificationTemplate)",
notes = "Create or update notification template.\n\n" +
"Example:\n" +
"```\n{\n \"name\": \"Hello to all my users\",\n" +
" \"notificationType\": \"Message from administrator\",\n" +
" \"configuration\": {\n" +
" \"defaultTextTemplate\": \"Hello everyone\", # required if any of the templates' bodies is not set\n" +
" \"templates\": {\n" +
" \"PUSH\": {\n \"method\": \"PUSH\",\n \"body\": null # defaultTextTemplate will be used if body is not set\n },\n" +
" \"SMS\": {\n \"method\": \"SMS\",\n \"body\": null\n },\n" +
" \"EMAIL\": {\n \"method\": \"EMAIL\",\n \"body\": \"Non-default value for email notification: <body>Hello everyone</body>\",\n \"subject\": \"Message from administrator\"\n },\n" +
" \"SLACK\": {\n \"method\": \"SLACK\",\n \"body\": null,\n \"conversationType\": \"PUBLIC_CHANNEL\",\n \"conversationId\": \"U02LD7BJOU2\" # received from listSlackConversations API method\n }\n" +
" }\n" +
" }\n}\n```" +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/template")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception {
notificationTemplate.setTenantId(getTenantId());
checkEntity(notificationTemplate.getId(), notificationTemplate, NOTIFICATION);
return doSaveAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::saveNotificationTemplate);
}
@ApiOperation(value = "Get notification template by id (getNotificationTemplateById)",
notes = "Fetch notification template by id." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/template/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTemplate getNotificationTemplateById(@PathVariable UUID id) throws ThingsboardException {
NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id);
return checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.READ);
}
@ApiOperation(value = "Get notification templates (getNotificationTemplates)",
notes = "Fetch the page of notification templates owned by sysadmin or tenant." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/templates")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTemplate> getNotificationTemplates(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) NotificationType[] notificationTypes,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (notificationTypes == null || notificationTypes.length == 0) {
notificationTypes = NotificationType.values();
}
return notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(user.getTenantId(),
List.of(notificationTypes), pageLink);
}
@ApiOperation(value = "Delete notification template by id (deleteNotificationTemplateById",
notes = "Delete notification template by its id.\n\n" +
"This template cannot be referenced by existing scheduled notification requests or any notification rules." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@DeleteMapping("/template/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationTemplateById(@PathVariable UUID id) throws Exception {
NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id);
NotificationTemplate notificationTemplate = checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::deleteNotificationTemplateById);
}
@ApiOperation(value = "List Slack conversations (listSlackConversations)",
notes = "List available Slack conversations by type to use in notification template.\n\n" +
"Slack must be configured in notification settings." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/slack/conversations")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<SlackConversation> listSlackConversations(@RequestParam SlackConversationType type,
@AuthenticationPrincipal SecurityUser user) {
// generic permission
NotificationSettings settings = notificationSettingsService.findNotificationSettings(user.getTenantId());
SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig)
settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK);
if (slackConfig == null) {
throw new IllegalArgumentException("Slack is not configured");
}
return slackService.listConversations(user.getTenantId(), slackConfig.getBotToken(), type);
}
}

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

@ -47,7 +47,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
import org.thingsboard.server.exception.ToErrorResponseEntity;
import javax.annotation.Nullable;
import java.util.UUID;

4
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -83,8 +83,8 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.telemetry.AttributeData;
import org.thingsboard.server.service.telemetry.TsData;
import org.thingsboard.server.service.telemetry.exception.InvalidParametersException;
import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;

15
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -21,6 +21,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -32,14 +34,19 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
@ -268,4 +275,12 @@ public class TenantProfileController extends BaseController {
throw handleException(e);
}
}
@GetMapping(value = "/tenantProfiles", params = {"ids"})
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) {
return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
}
}

5
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -58,9 +57,9 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.UserSettings;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -70,7 +69,6 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -109,7 +107,6 @@ public class UserController extends BaseController {
public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s";
@Value("${security.user_token_access_enabled}")
@Getter
private boolean userTokenAccessEnabled;
private final MailService mailService;

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

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
@ -40,10 +41,11 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.telemetry.SessionEvent;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.ws.SessionEvent;
import org.thingsboard.server.service.ws.WebSocketMsgEndpoint;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketSessionType;
import javax.websocket.RemoteEndpoint;
import javax.websocket.SendHandler;
@ -59,20 +61,21 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService.NUMBER_OF_PING_ATTEMPTS;
import static org.thingsboard.server.service.ws.DefaultWebSocketService.NUMBER_OF_PING_ATTEMPTS;
@Service
@TbCoreComponent
@Slf4j
public class TbWebSocketHandler extends TextWebSocketHandler implements TelemetryWebSocketMsgEndpoint {
public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocketMsgEndpoint {
private final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired
private TelemetryWebSocketService webSocketService;
@Autowired @Lazy
private WebSocketService webSocketService;
@Autowired
private TbTenantProfileCache tenantProfileCache;
@ -82,7 +85,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
@Value("${server.ws.ping_timeout:30000}")
private long pingTimeout;
private final ConcurrentMap<String, TelemetryWebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private final ConcurrentMap<String, WebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
@ -133,7 +136,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
String internalSessionId = session.getId();
TelemetryWebSocketSessionRef sessionRef = toRef(session);
WebSocketSessionRef sessionRef = toRef(session);
String externalSessionId = sessionRef.getSessionId();
if (!checkLimits(session, sessionRef)) {
@ -142,7 +145,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef,
tenantProfileConfiguration != null && tenantProfileConfiguration.getWsMsgQueueLimitPerSession() > 0 ?
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500));
tenantProfileConfiguration.getWsMsgQueueLimitPerSession() : 500));
externalSessionMap.put(externalSessionId, internalSessionId);
processInWebSocketService(sessionRef, SessionEvent.onEstablished());
@ -182,7 +185,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
private void processInWebSocketService(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
private void processInWebSocketService(WebSocketSessionRef sessionRef, SessionEvent event) {
try {
webSocketService.handleWebSocketSessionEvent(sessionRef, event);
} catch (BeanCreationNotAllowedException e) {
@ -190,7 +193,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
private TelemetryWebSocketSessionRef toRef(WebSocketSession session) throws IOException {
private WebSocketSessionRef toRef(WebSocketSession session) throws IOException {
URI sessionUri = session.getUri();
String path = sessionUri.getPath();
path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length());
@ -199,25 +202,30 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
String[] pathElements = path.split("/");
String serviceToken = pathElements[0];
if (!"telemetry".equalsIgnoreCase(serviceToken)) {
throw new InvalidParameterException("Can't find plugin with specified token!");
} else {
SecurityUser currentUser = (SecurityUser) ((Authentication) session.getPrincipal()).getPrincipal();
return new TelemetryWebSocketSessionRef(UUID.randomUUID().toString(), currentUser, session.getLocalAddress(), session.getRemoteAddress());
}
WebSocketSessionType sessionType = WebSocketSessionType.forName(serviceToken)
.orElseThrow(() -> new InvalidParameterException("Can't find plugin with specified token!"));
SecurityUser currentUser = (SecurityUser) ((Authentication) session.getPrincipal()).getPrincipal();
return WebSocketSessionRef.builder()
.sessionId(UUID.randomUUID().toString())
.securityCtx(currentUser)
.localAddress(session.getLocalAddress())
.remoteAddress(session.getRemoteAddress())
.sessionType(sessionType)
.build();
}
private class SessionMetaData implements SendHandler {
private final WebSocketSession session;
private final RemoteEndpoint.Async asyncRemote;
private final TelemetryWebSocketSessionRef sessionRef;
private final WebSocketSessionRef sessionRef;
private volatile boolean isSending = false;
private final AtomicBoolean isSending = new AtomicBoolean(false);
private final Queue<TbWebSocketMsg<?>> msgQueue;
private volatile long lastActivityTime;
SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef, int maxMsgQueuePerSession) {
SessionMetaData(WebSocketSession session, WebSocketSessionRef sessionRef, int maxMsgQueuePerSession) {
super();
this.session = session;
Session nativeSession = ((NativeWebSocketSession) session).getNativeSession(Session.class);
@ -259,7 +267,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
synchronized void sendMsg(TbWebSocketMsg<?> msg) {
if (isSending) {
if (isSending.compareAndSet(false, true)) {
sendMsgInternal(msg);
} else {
try {
msgQueue.add(msg);
} catch (RuntimeException e) {
@ -270,9 +280,6 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!"));
}
} else {
isSending = true;
sendMsgInternal(msg);
}
}
@ -307,13 +314,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
if (msg != null) {
sendMsgInternal(msg);
} else {
isSending = false;
isSending.set(false);
}
}
}
@Override
public void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException {
public void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException {
String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing {}", externalId, msg);
String internalId = externalSessionMap.get(externalId);
@ -349,7 +356,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
@Override
public void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException {
public void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException {
String externalId = sessionRef.getSessionId();
String internalId = externalSessionMap.get(externalId);
if (internalId != null) {
@ -365,7 +372,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
@Override
public void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus reason) throws IOException {
public void close(WebSocketSessionRef sessionRef, CloseStatus reason) throws IOException {
String externalId = sessionRef.getSessionId();
log.debug("[{}] Processing close request", externalId);
String internalId = externalSessionMap.get(externalId);
@ -381,7 +388,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
private boolean checkLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) throws Exception {
private boolean checkLimits(WebSocketSession session, WebSocketSessionRef sessionRef) throws Exception {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) {
return true;
@ -448,7 +455,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
return true;
}
private void cleanupLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) {
private void cleanupLimits(WebSocketSession session, WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) return;
@ -483,9 +490,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
}
}
private DefaultTenantProfileConfiguration getTenantProfileConfiguration(TelemetryWebSocketSessionRef sessionRef) {
private DefaultTenantProfileConfiguration getTenantProfileConfiguration(WebSocketSessionRef sessionRef) {
return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()))
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
}
}
}

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java → application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java → application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java → application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java → application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java → application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java → application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

2
application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java → application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.exception;
package org.thingsboard.server.exception;
import org.springframework.http.ResponseEntity;

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

@ -35,6 +35,9 @@ import org.thingsboard.server.service.install.migrate.EntitiesMigrateService;
import org.thingsboard.server.service.install.migrate.TsLatestMigrateService;
import org.thingsboard.server.service.install.update.CacheCleanupService;
import org.thingsboard.server.service.install.update.DataUpdateService;
import org.thingsboard.server.service.install.update.DefaultDataUpdateService;
import static org.thingsboard.server.service.install.update.DefaultDataUpdateService.getEnv;
@Service
@Profile("install")
@ -246,6 +249,11 @@ public class ThingsboardInstallService {
databaseEntitiesUpgradeService.upgradeDatabase("3.4.4");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
if (!getEnv("SKIP_DEFAULT_NOTIFICATION_CONFIGS_CREATION", false)) {
systemDataLoaderService.createDefaultNotificationConfigs();
} else {
log.info("Skipping default notification configs creation");
}
installScripts.loadSystemLwm2mResources();
break;
//TODO update CacheCleanupService on the next version upgrade

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

@ -29,6 +29,7 @@ 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.AlarmComment;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -95,6 +96,12 @@ public class EntityActionService {
case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE;
break;
case ADDED_COMMENT:
msgType = DataConstants.COMMENT_CREATED;
break;
case UPDATED_COMMENT:
msgType = DataConstants.COMMENT_UPDATED;
break;
case ASSIGNED_FROM_TENANT:
msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT;
break;
@ -169,6 +176,9 @@ public class EntityActionService {
String strEdgeName = extractParameter(String.class, 2, additionalInfo);
metaData.putValue("unassignedEdgeId", strEdgeId);
metaData.putValue("unassignedEdgeName", strEdgeName);
} else if (actionType == ActionType.ADDED_COMMENT || actionType == ActionType.UPDATED_COMMENT) {
AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo);
metaData.putValue("comment", json.writeValueAsString(comment));
}
ObjectNode entityNode;
if (entity != null) {
@ -176,6 +186,8 @@ public class EntityActionService {
if (entityId.getEntityType() == EntityType.DASHBOARD) {
entityNode.put("configuration", "");
}
metaData.putValue("entityName", entity.getName());
metaData.putValue("entityType", entityId.getEntityType().toString());
} else {
entityNode = json.createObjectNode();
if (actionType == ActionType.ATTRIBUTES_UPDATED) {

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

@ -17,6 +17,7 @@ package org.thingsboard.server.service.apiusage;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import org.thingsboard.server.dao.notification.NotificationRuleProcessingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -86,6 +88,7 @@ import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService<EntityId> implements TbApiUsageStateService {
public static final String HOURLY = "Hourly";
@ -105,6 +108,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
private final ApiUsageStateService apiUsageStateService;
private final TbTenantProfileCache tenantProfileCache;
private final MailService mailService;
private final NotificationRuleProcessingService notificationRuleProcessingService;
private final DbCallbackExecutorService dbExecutor;
@Lazy
@ -126,26 +130,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
private final Lock updateLock = new ReentrantLock();
private final ExecutorService mailExecutor;
public DefaultTbApiUsageStateService(TbClusterService clusterService,
PartitionService partitionService,
TenantService tenantService,
TimeseriesService tsService,
ApiUsageStateService apiUsageStateService,
TbTenantProfileCache tenantProfileCache,
MailService mailService,
DbCallbackExecutorService dbExecutor) {
this.clusterService = clusterService;
this.partitionService = partitionService;
this.tenantService = tenantService;
this.tsService = tsService;
this.apiUsageStateService = apiUsageStateService;
this.tenantProfileCache = tenantProfileCache;
this.mailService = mailService;
this.mailExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("api-usage-svc-mail"));
this.dbExecutor = dbExecutor;
}
private final ExecutorService mailExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("api-usage-svc-mail"));
@PostConstruct
public void init() {
@ -355,6 +340,7 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK);
if (state.getEntityType() == EntityType.TENANT && !state.getEntityId().equals(TenantId.SYS_TENANT_ID)) {
String email = tenantService.findTenantById(state.getTenantId()).getEmail();
if (StringUtils.isNotEmpty(email)) {
result.forEach((apiFeature, stateValue) -> {

28
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java → application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java

@ -13,19 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage;
package org.thingsboard.server.service.apiusage.limits;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
@ -33,28 +31,22 @@ public class DefaultRateLimitService implements RateLimitService {
private final TbTenantProfileCache tenantProfileCache;
private final Map<String, Map<TenantId, TbRateLimits>> rateLimits = new ConcurrentHashMap<>();
private final Map<LimitedApi, Map<TenantId, TbRateLimits>> rateLimits = new ConcurrentHashMap<>();
@Override
public boolean checkEntityExportLimit(TenantId tenantId) {
return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit);
}
@Override
public boolean checkEntityImportLimit(TenantId tenantId) {
return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit);
}
private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function<DefaultTenantProfileConfiguration, String> rateLimitConfigExtractor) {
public boolean checkRateLimit(TenantId tenantId, LimitedApi api) {
if (tenantId.isSysTenantId()) {
return true;
}
String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
.map(rateLimitConfigExtractor).orElse(null);
.map(api::getLimitConfig).orElse(null);
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(rateLimitsKey);
Map<TenantId, TbRateLimits> rateLimits = this.rateLimits.get(api);
if (StringUtils.isEmpty(rateLimitConfig)) {
if (rateLimits != null) {
rateLimits.remove(tenantId);
if (rateLimits.isEmpty()) {
this.rateLimits.remove(rateLimitsKey);
this.rateLimits.remove(api);
}
}
return true;
@ -62,7 +54,7 @@ public class DefaultRateLimitService implements RateLimitService {
if (rateLimits == null) {
rateLimits = new ConcurrentHashMap<>();
this.rateLimits.put(rateLimitsKey, rateLimits);
this.rateLimits.put(api, rateLimits);
}
TbRateLimits rateLimit = rateLimits.get(tenantId);
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {

36
application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage.limits;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.function.Function;
@RequiredArgsConstructor
public enum LimitedApi {
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit),
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit),
NOTIFICATION_REQUEST(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit);
private final Function<DefaultTenantProfileConfiguration, String> configExtractor;
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) {
return configExtractor.apply(profileConfiguration);
}
}

6
application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java → application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java

@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.apiusage;
package org.thingsboard.server.service.apiusage.limits;
import org.thingsboard.server.common.data.id.TenantId;
public interface RateLimitService {
boolean checkEntityExportLimit(TenantId tenantId);
boolean checkEntityImportLimit(TenantId tenantId);
boolean checkRateLimit(TenantId tenantId, LimitedApi api);
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -87,7 +87,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
return handleUnsupportedMsgType(deviceUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("Can't create more then")) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed devices violated {}", tenantId, deviceUpdateMsg, e);
return Futures.immediateFuture(null);
} else {

8
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -34,7 +35,6 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.edge.EdgeService;
@ -63,15 +63,13 @@ public abstract class AbstractTbEntityService {
protected EdgeService edgeService;
@Autowired
protected AlarmService alarmService;
@Autowired
@Autowired @Lazy
protected AlarmSubscriptionService alarmSubscriptionService;
@Autowired
protected AlarmCommentService alarmCommentService;
@Autowired
protected CustomerService customerService;
@Autowired
protected TbClusterService tbClusterService;
@Autowired(required = false)
@Autowired(required = false) @Lazy
private EntitiesVersionControlService vcService;
protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {

3
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Tenant;
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.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -221,7 +222,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
}
@Override
public void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo) {
public void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo) {
logEntityAction(alarm.getTenantId(), alarm.getOriginator(), alarm, alarm.getCustomerId(), actionType, user, additionalInfo);
sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType));
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.Tenant;
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.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -100,7 +101,7 @@ public interface TbNotificationEntityService {
void notifyCreateOrUpdateOrDeleteEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType,
User user, Object... additionalInfo);
void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo);
void notifyCreateOrUpdateAlarm(AlarmInfo alarm, ActionType actionType, User user, Object... additionalInfo);
void notifyAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user);

11
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.alarm;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
@ -27,16 +28,22 @@ 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.UserId;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service
@AllArgsConstructor
public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{
@Autowired
private AlarmCommentService alarmCommentService;
@Override
public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException {
ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED_COMMENT : ActionType.UPDATED_COMMENT;
UserId userId = user.getId();
alarmComment.setUserId(userId);
if (user != null) {
alarmComment.setUserId(user.getId());
}
try {
AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment));
notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user);

46
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -16,6 +16,8 @@
package org.thingsboard.server.service.entitiy.alarm;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
@ -24,9 +26,9 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmAssignee;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -40,8 +42,12 @@ import java.util.List;
@Service
@AllArgsConstructor
@Slf4j
public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService {
@Autowired
protected TbAlarmCommentService alarmCommentService;
@Override
public Alarm save(Alarm alarm, User user) throws ThingsboardException {
ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
@ -70,7 +76,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
UserId newAssignee = alarm.getAssigneeId();
UserId curAssignee = resultAlarm.getAssigneeId();
if (newAssignee != null && !newAssignee.equals(curAssignee)) {
resultAlarm = assign(alarm, newAssignee, alarm.getAssignTs(), user);
resultAlarm = assign(resultAlarm, newAssignee, alarm.getAssignTs(), user);
} else if (newAssignee == null && curAssignee != null) {
resultAlarm = unassign(alarm, alarm.getAssignTs(), user);
}
@ -97,11 +103,15 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ACK"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user);
} else {
throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
@ -125,11 +135,15 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "CLEAR"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user);
} else {
throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
@ -150,13 +164,17 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(),
(assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName()))
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(),
(assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName()))
.put("userId", user.getId().toString())
.put("assigneeId", assignee.getId().toString())
.put("subtype", "ASSIGN"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user);
} else {
throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
@ -176,11 +194,15 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user);
} else {
throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
@ -200,4 +222,4 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
private static long getOrDefault(long ts) {
return ts > 0 ? ts : System.currentTimeMillis();
}
}
}

33
application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java

@ -0,0 +1,33 @@
/**
* 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.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class NotificationExecutorService extends AbstractListeningExecutor {
@Value("${notification_system.thread_pool_size:10}")
private int threadPoolSize;
@Override
protected int getThreadPollSize() {
return threadPoolSize;
}
}

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

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
@ -62,6 +63,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
@ -82,13 +84,13 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@ -97,6 +99,7 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@ -171,6 +174,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private JwtSettingsService jwtSettingsService;
@Autowired
private NotificationSettingsService notificationSettingsService;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@ -671,4 +677,31 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
}
}
@Override
public void createDefaultNotificationConfigs() {
try {
log.info("Creating default notification configs for system admin");
notificationSettingsService.createDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
} catch (Exception e) {
if (StringUtils.contains(e.getMessage(), "already exists")) {
log.info("Default notification configs are already present for system admin, skipping");
} else {
throw e;
}
}
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500);
log.info("Creating default notification configs for all tenants");
for (TenantId tenantId : tenants) {
try {
notificationSettingsService.createDefaultNotificationConfigs(tenantId);
} catch (Exception e) {
if (StringUtils.contains(e.getMessage(), "already exists")) {
log.info("Default notification configs are already present for tenant {}, skipping", tenantId);
} else {
throw e;
}
}
}
}
}

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

@ -38,4 +38,7 @@ public interface SystemDataLoaderService {
void deleteSystemWidgetBundle(String bundleAlias) throws Exception;
void createQueues();
void createDefaultNotificationConfigs();
}

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

@ -672,7 +672,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
return mainQueueConfiguration;
}
private boolean getEnv(String name, boolean defaultValue) {
public static boolean getEnv(String name, boolean defaultValue) {
String env = System.getenv(name);
if (env == null) {
return defaultValue;

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

@ -0,0 +1,431 @@
/**
* 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;
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.MailService;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
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.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
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.UsersFilterType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.notification.NotificationRequestService;
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.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.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.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;
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;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings({"UnstableApiUsage", "rawtypes"})
public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel<User, WebDeliveryMethodNotificationTemplate> {
private final NotificationTargetService notificationTargetService;
private final NotificationRequestService notificationRequestService;
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;
private final MailService mailService;
private final SmsService smsService;
private Map<NotificationDeliveryMethod, NotificationChannel> channels;
@Override
public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) {
if (!rateLimitService.checkRateLimit(tenantId, LimitedApi.NOTIFICATION_REQUEST)) {
throw new TbRateLimitsException(EntityType.TENANT);
}
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
NotificationTemplate notificationTemplate;
if (notificationRequest.getTemplateId() != null) {
notificationTemplate = notificationTemplateService.findNotificationTemplateById(tenantId, notificationRequest.getTemplateId());
} else {
notificationTemplate = notificationRequest.getTemplate();
}
if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing");
List<NotificationTarget> targets = notificationRequest.getTargets().stream().map(NotificationTargetId::new)
.map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)).collect(Collectors.toList());
Set<NotificationDeliveryMethod> availableDeliveryMethods = getAvailableDeliveryMethods(tenantId);
notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (!template.isEnabled()) return;
if (!availableDeliveryMethods.contains(deliveryMethod)) {
throw new IllegalArgumentException("Settings for " + deliveryMethod.getName() + " are missing");
}
if (notificationRequest.getRuleId() == null) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing");
}
}
});
if (notificationRequest.getAdditionalConfig() != null) {
NotificationRequestConfig config = notificationRequest.getAdditionalConfig();
if (config.getSendingDelayInSec() > 0 && notificationRequest.getId() == null) {
notificationRequest.setStatus(NotificationRequestStatus.SCHEDULED);
NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest);
forwardToNotificationSchedulerService(tenantId, savedNotificationRequest.getId());
return savedNotificationRequest;
}
}
log.debug("Processing notification request (tenantId: {}, targets: {})", tenantId, notificationRequest.getTargets());
notificationRequest.setStatus(NotificationRequestStatus.PROCESSING);
NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest);
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId)
.request(savedNotificationRequest)
.settings(settings)
.template(notificationTemplate)
.build();
notificationExecutor.submit(() -> {
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationTarget target : targets) {
List<ListenableFuture<Void>> result = processForTarget(target, ctx);
results.addAll(result);
}
Futures.whenAllComplete(results).run(() -> {
NotificationRequestId requestId = savedNotificationRequest.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);
}
}, dbCallbackExecutorService);
});
return savedNotificationRequest;
}
private List<ListenableFuture<Void>> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) {
Iterable<? extends NotificationRecipient> recipients;
switch (target.getConfiguration().getType()) {
case PLATFORM_USERS: {
PlatformUsersNotificationTargetConfig platformUsersTargetConfig = (PlatformUsersNotificationTargetConfig) target.getConfiguration();
if (platformUsersTargetConfig.getUsersFilter().getType() == UsersFilterType.AFFECTED_USER) {
if (ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) {
UserId targetUserId = ((RuleOriginatedNotificationInfo) ctx.getRequest().getInfo()).getTargetUserId();
if (targetUserId != null) {
recipients = List.of(userService.findUserById(ctx.getTenantId(), targetUserId));
break;
}
}
recipients = Collections.emptyList();
} else {
recipients = new PageDataIterable<>(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), ctx.getCustomerId(), platformUsersTargetConfig, pageLink);
}, 500);
}
break;
}
case SLACK: {
SlackNotificationTargetConfig slackTargetConfig = (SlackNotificationTargetConfig) target.getConfiguration();
recipients = List.of(slackTargetConfig.getConversation());
break;
}
default: {
recipients = Collections.emptyList();
}
}
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);
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);
}
}
}
return results;
}
private ListenableFuture<Void> processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) {
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) {
return Futures.immediateFailedFuture(new AlreadySentException());
}
Map<String, String> templateContext;
if (recipient instanceof User) {
templateContext = ctx.createTemplateContext(((User) recipient));
} else {
templateContext = Collections.emptyMap();
}
DeliveryMethodNotificationTemplate processedTemplate;
try {
processedTemplate = ctx.getProcessedTemplate(deliveryMethod, templateContext);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
NotificationChannel notificationChannel = channels.get(deliveryMethod);
log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient);
return notificationChannel.sendNotification(recipient, processedTemplate, ctx);
}
@Override
public ListenableFuture<Void> sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
NotificationRequest request = ctx.getRequest();
Notification notification = Notification.builder()
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationTemplate().getNotificationType())
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
.info(request.getInfo())
.status(NotificationStatus.SENT)
.build();
try {
notification = notificationService.saveNotification(recipient.getTenantId(), notification);
} catch (Exception e) {
log.error("Failed to create notification for recipient {}", recipient.getId(), e);
return Futures.immediateFailedFuture(e);
}
NotificationUpdate update = NotificationUpdate.builder()
.created(true)
.notification(notification)
.build();
return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update);
}
@Override
public void sendBasicNotification(TenantId tenantId, UserId recipientId, String subject, String text) {
Notification notification = Notification.builder()
.recipientId(recipientId)
.type(NotificationType.GENERAL)
.subject(subject)
.text(text)
.status(NotificationStatus.SENT)
.build();
notification = notificationService.saveNotification(TenantId.SYS_TENANT_ID, notification);
NotificationUpdate update = NotificationUpdate.builder()
.created(true)
.notification(notification)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
@Override
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) {
log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
.notificationId(notificationId)
.newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
}
@Override
public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId);
if (updatedCount > 0) {
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
.allNotifications(true)
.newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
}
@Override
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId);
if (deleted) {
NotificationUpdate update = NotificationUpdate.builder()
.deleted(true)
.notification(notification)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
}
@Override
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) {
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>();
deliveryMethods.add(NotificationDeliveryMethod.WEB);
NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId);
if (notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK)) {
deliveryMethods.add(NotificationDeliveryMethod.SLACK);
}
try {
mailService.testConnection(tenantId);
deliveryMethods.add(NotificationDeliveryMethod.EMAIL);
} catch (Exception e) {}
if (smsService.isConfigured(tenantId)) {
deliveryMethods.add(NotificationDeliveryMethod.SMS);
}
return deliveryMethods;
}
@Override
public void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId) {
log.debug("Deleting notification request {}", notificationRequestId);
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId);
notificationRequestService.deleteNotificationRequest(tenantId, notificationRequestId);
if (notificationRequest.isSent()) {
// TODO: no need to send request update for other than PLATFORM_USERS target type
onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder()
.notificationRequestId(notificationRequestId)
.deleted(true)
.build());
} else if (notificationRequest.isScheduled()) {
clusterService.broadcastEntityStateChangeEvent(tenantId, notificationRequestId, ComponentLifecycleEvent.DELETED);
}
}
private void forwardToNotificationSchedulerService(TenantId tenantId, NotificationRequestId notificationRequestId) {
TransportProtos.NotificationSchedulerServiceMsg.Builder msg = TransportProtos.NotificationSchedulerServiceMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRequestIdMSB(notificationRequestId.getId().getMostSignificantBits())
.setRequestIdLSB(notificationRequestId.getId().getLeastSignificantBits())
.setTs(System.currentTimeMillis());
TransportProtos.ToCoreMsg toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setNotificationSchedulerServiceMsg(msg)
.build();
clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null);
}
private ListenableFuture<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);
}
private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) {
log.trace("Submitting notification request update: {}", update);
wsCallBackExecutor.submit(() -> {
TransportProtos.ToCoreNotificationMsg notificationRequestUpdateProto = TbSubscriptionUtils.notificationRequestUpdateToProto(tenantId, update);
Set<String> coreServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_CORE));
for (String serviceId : coreServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId);
producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), notificationRequestUpdateProto), null);
}
});
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.WEB;
}
@Override
protected String getExecutorPrefix() {
return "notification";
}
@Autowired
public void setChannels(List<NotificationChannel> channels, NotificationCenter webNotificationChannel) {
this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c));
this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel);
}
}

176
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java

@ -0,0 +1,176 @@
/**
* 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;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService {
private final NotificationCenter notificationCenter;
private final NotificationRequestService notificationRequestService;
private final SchedulerComponent scheduler;
private final NotificationExecutorService notificationExecutor;
private final Map<NotificationRequestId, ScheduledRequestMetadata> scheduledNotificationRequests = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
super.init();
}
@Override
protected Map<TopicPartitionInfo, List<ListenableFuture<?>>> onAddedPartitions(Set<TopicPartitionInfo> addedPartitions) {
PageDataIterable<NotificationRequest> notificationRequests = new PageDataIterable<>(pageLink -> {
return notificationRequestService.findScheduledNotificationRequests(pageLink);
}, 1000);
for (NotificationRequest notificationRequest : notificationRequests) {
TopicPartitionInfo requestPartition = partitionService.resolve(ServiceType.TB_CORE, notificationRequest.getTenantId(), notificationRequest.getId());
if (addedPartitions.contains(requestPartition)) {
partitionedEntities.computeIfAbsent(requestPartition, k -> ConcurrentHashMap.newKeySet()).add(notificationRequest.getId());
if (!scheduledNotificationRequests.containsKey(notificationRequest.getId())) {
scheduleNotificationRequest(notificationRequest.getTenantId(), notificationRequest, notificationRequest.getCreatedTime());
}
}
}
return Collections.emptyMap();
}
@Override
public void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs) {
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId);
scheduleNotificationRequest(tenantId, notificationRequest, requestTs);
}
private void scheduleNotificationRequest(TenantId tenantId, NotificationRequest request, long requestTs) {
int delayInSec = Optional.ofNullable(request)
.map(NotificationRequest::getAdditionalConfig)
.map(NotificationRequestConfig::getSendingDelayInSec)
.orElse(0);
if (delayInSec <= 0) return;
long delayInMs = TimeUnit.SECONDS.toMillis(delayInSec) - (System.currentTimeMillis() - requestTs);
if (delayInMs < 0) {
delayInMs = 0;
}
ScheduledFuture<?> scheduledTask = scheduler.schedule(() -> {
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, request.getId());
if (notificationRequest == null) return;
notificationExecutor.executeAsync(() -> {
try {
notificationCenter.processNotificationRequest(tenantId, notificationRequest);
} catch (Exception e) {
log.error("Failed to process scheduled notification request {}", notificationRequest.getId(), e);
UserId senderId = notificationRequest.getSenderId();
if (senderId != null) {
notificationCenter.sendBasicNotification(tenantId, senderId, "Notification failure",
"Failed to process scheduled notification (request " + notificationRequest.getId() + "): " + e.getMessage());
}
}
});
scheduledNotificationRequests.remove(notificationRequest.getId());
}, delayInMs, TimeUnit.MILLISECONDS);
scheduledNotificationRequests.put(request.getId(), new ScheduledRequestMetadata(tenantId, scheduledTask));
}
@EventListener(ComponentLifecycleMsg.class)
public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) {
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
EntityId entityId = event.getEntityId();
switch (entityId.getEntityType()) {
case NOTIFICATION_REQUEST:
cancelAndRemove((NotificationRequestId) entityId);
break;
case TENANT:
Set<NotificationRequestId> toCancel = new HashSet<>();
scheduledNotificationRequests.forEach((notificationRequestId, scheduledRequestMetadata) -> {
if (scheduledRequestMetadata.getTenantId().equals(entityId)) {
toCancel.add(notificationRequestId);
}
});
toCancel.forEach(this::cancelAndRemove);
break;
}
}
}
@Override
protected void cleanupEntityOnPartitionRemoval(NotificationRequestId notificationRequestId) {
cancelAndRemove(notificationRequestId);
}
private void cancelAndRemove(NotificationRequestId notificationRequestId) {
ScheduledRequestMetadata md = scheduledNotificationRequests.remove(notificationRequestId);
if (md != null) {
md.getFuture().cancel(false);
}
}
@Override
protected String getServiceName() {
return "Notifications scheduler";
}
@Override
protected String getSchedulerExecutorName() {
return "notifications-scheduler";
}
@Data
private static class ScheduledRequestMetadata {
private final TenantId tenantId;
private final ScheduledFuture<?> future;
}
}

163
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -0,0 +1,163 @@
/**
* 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;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.base.Strings;
import lombok.Builder;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.HasSubject;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
@SuppressWarnings("unchecked")
public class NotificationProcessingContext {
@Getter
private final TenantId tenantId;
private final NotificationSettings settings;
@Getter
private final NotificationRequest request;
@Getter
private final NotificationTemplate notificationTemplate;
private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter
private Set<NotificationDeliveryMethod> deliveryMethods;
@Getter
private final NotificationRequestStats stats;
private static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\$\\{([a-zA-Z]+)(:[a-zA-Z]+)?}");
@Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings,
NotificationTemplate template) {
this.tenantId = tenantId;
this.request = request;
this.settings = settings;
this.notificationTemplate = template;
this.templates = new EnumMap<>(NotificationDeliveryMethod.class);
this.stats = new NotificationRequestStats();
init();
}
private void init() {
NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration();
templateConfig.getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> {
if (template.isEnabled()) {
templates.put(deliveryMethod, template);
}
});
deliveryMethods = templates.keySet();
}
public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) {
return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod);
}
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map<String, String> templateContext) {
NotificationInfo info = request.getInfo();
if (info != null) {
templateContext = new HashMap<>(templateContext);
templateContext.putAll(info.getTemplateData());
}
T template = (T) templates.get(deliveryMethod).copy();
template.setBody(processTemplate(template.getBody(), templateContext));
if (template instanceof HasSubject) {
String subject = ((HasSubject) template).getSubject();
((HasSubject) template).setSubject(processTemplate(subject, templateContext));
}
if (deliveryMethod == NotificationDeliveryMethod.WEB) {
WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template;
Optional<ObjectNode> buttonConfig = Optional.ofNullable(webNotificationTemplate.getAdditionalConfig())
.map(config -> config.get("actionButtonConfig")).filter(JsonNode::isObject)
.map(config -> (ObjectNode) config);
if (buttonConfig.isPresent()) {
JsonNode text = buttonConfig.get().get("text");
if (text != null && text.isTextual()) {
text = new TextNode(processTemplate(text.asText(), templateContext));
buttonConfig.get().set("text", text);
}
JsonNode link = buttonConfig.get().get("link");
if (link != null && link.isTextual()) {
link = new TextNode(processTemplate(link.asText(), templateContext));
buttonConfig.get().set("link", link);
}
}
}
return template;
}
private static String processTemplate(String template, Map<String, String> context) {
return TEMPLATE_PARAM_PATTERN.matcher(template).replaceAll(matchResult -> {
String key = matchResult.group(1);
String value = Strings.nullToEmpty(context.get(key));
String function = matchResult.group(2);
if (function != null) {
switch (function) {
case ":upperCase":
return value.toUpperCase();
case ":lowerCase":
return value.toLowerCase();
case ":capitalize":
return StringUtils.capitalize(value.toLowerCase());
}
}
return value;
});
}
public Map<String, String> createTemplateContext(User recipient) {
Map<String, String> templateContext = new HashMap<>();
templateContext.put("recipientEmail", recipient.getEmail());
templateContext.put("recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()));
templateContext.put("recipientLastName", Strings.nullToEmpty(recipient.getLastName()));
return templateContext;
}
public CustomerId getCustomerId() {
if (request.getInfo() instanceof RuleOriginatedNotificationInfo) {
return ((RuleOriginatedNotificationInfo) request.getInfo()).getOriginatorEntityCustomerId();
} else {
return null;
}
}
}

25
application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java

@ -0,0 +1,25 @@
/**
* 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;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
public interface NotificationSchedulerService {
void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs);
}

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

@ -0,0 +1,48 @@
/**
* 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.channels;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
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
@RequiredArgsConstructor
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.sendEmail(recipient.getTenantId(), recipient.getEmail(), processedTemplate.getSubject(), processedTemplate.getBody());
return null;
});
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.EMAIL;
}
}

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

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
public interface NotificationChannel<R extends NotificationRecipient, T extends DeliveryMethodNotificationTemplate> {
ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
NotificationDeliveryMethod getDeliveryMethod();
}

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

@ -0,0 +1,50 @@
/**
* 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.channels;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
@Component
@RequiredArgsConstructor
public class SlackNotificationChannel implements NotificationChannel<SlackConversation, SlackDeliveryMethodNotificationTemplate> {
private final SlackService slackService;
private final ExternalCallExecutorService executor;
@Override
public ListenableFuture<Void> sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK);
return executor.submit(() -> {
slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody());
return null;
});
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.SLACK;
}
}

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

@ -0,0 +1,55 @@
/**
* 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.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;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.User;
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) {
String phone = recipient.getPhone();
if (StringUtils.isBlank(phone)) {
return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number"));
}
return executor.submit(() -> {
smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody());
return null;
});
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.SMS;
}
}

201
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java

@ -0,0 +1,201 @@
/**
* 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;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.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.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.NotificationRuleTrigger;
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.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleProcessingService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor;
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;
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"rawtypes", "unchecked"})
public class DefaultNotificationRuleProcessingService implements NotificationRuleProcessingService {
private final NotificationRuleService notificationRuleService;
private final NotificationRequestService notificationRequestService;
@Autowired @Lazy
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
private final Map<String, NotificationRuleTriggerType> ruleEngineMsgTypeToTriggerType = new HashMap<>();
@Override
public void process(TenantId tenantId, NotificationRuleTrigger trigger) {
List<NotificationRule> rules = notificationRuleService.findNotificationRulesByTenantIdAndTriggerType(
trigger.getType().isTenantLevel() ? tenantId : TenantId.SYS_TENANT_ID, trigger.getType());
for (NotificationRule rule : rules) {
notificationExecutor.submit(() -> {
try {
processNotificationRule(tenantId, rule, trigger);
} catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
}
});
}
}
@Override
public void process(TenantId tenantId, TbMsg ruleEngineMsg) {
NotificationRuleTriggerType triggerType = ruleEngineMsgTypeToTriggerType.get(ruleEngineMsg.getType());
if (triggerType == null) {
return;
}
process(tenantId, RuleEngineMsgTrigger.builder()
.msg(ruleEngineMsg)
.triggerType(triggerType)
.build());
}
private void processNotificationRule(TenantId tenantId, NotificationRule rule, NotificationRuleTrigger trigger) {
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig();
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType());
if (matchesClearRule(trigger, triggerConfig)) {
List<NotificationRequest> notificationRequests = notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(tenantId, rule.getId(), trigger.getOriginatorEntityId());
if (notificationRequests.isEmpty()) {
return;
}
List<UUID> targets = notificationRequests.stream()
.filter(NotificationRequest::isSent)
.flatMap(notificationRequest -> notificationRequest.getTargets().stream())
.distinct().collect(Collectors.toList());
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
submitNotificationRequest(tenantId, targets, rule, trigger.getOriginatorEntityId(), notificationInfo, 0);
notificationRequests.forEach(notificationRequest -> {
if (notificationRequest.isScheduled()) {
notificationCenter.deleteNotificationRequest(tenantId, notificationRequest.getId());
}
});
return;
}
if (matchesFilter(trigger, triggerConfig)) {
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig);
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> {
submitNotificationRequest(tenantId, targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay);
});
}
}
private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) {
return triggerProcessors.get(triggerConfig.getTriggerType()).matchesFilter(trigger, triggerConfig);
}
private boolean matchesClearRule(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) {
return triggerProcessors.get(triggerConfig.getTriggerType()).matchesClearRule(trigger, triggerConfig);
}
private NotificationInfo constructNotificationInfo(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) {
return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger, triggerConfig);
}
private void submitNotificationRequest(TenantId tenantId, List<UUID> targets, NotificationRule rule,
EntityId originatorEntityId, NotificationInfo notificationInfo, int delayInSec) {
NotificationRequestConfig config = new NotificationRequestConfig();
if (delayInSec > 0) {
config.setSendingDelayInSec(delayInSec);
}
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.targets(targets)
.templateId(rule.getTemplateId())
.additionalConfig(config)
.info(notificationInfo)
.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(tenantId, notificationRequest);
} catch (Exception e) {
log.error("Failed to process notification request for rule {}", rule.getId(), e);
}
});
}
@EventListener(ComponentLifecycleMsg.class)
public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) {
if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED ||
componentLifecycleMsg.getEntityId().getEntityType() != EntityType.NOTIFICATION_RULE) {
return;
}
TenantId tenantId = componentLifecycleMsg.getTenantId();
NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId();
notificationExecutor.submit(() -> {
List<NotificationRequestId> scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId);
for (NotificationRequestId notificationRequestId : scheduledForRule) {
notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId);
}
});
}
@Autowired
public void setTriggerProcessors(Collection<NotificationRuleTriggerProcessor> processors) {
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());
});
}
});
}
}

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

@ -0,0 +1,82 @@
/**
* 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 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.notification.info.AlarmAssignmentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
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.dao.notification.trigger.RuleEngineMsgTrigger;
import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? Action.ASSIGNED : Action.UNASSIGNED;
if (!triggerConfig.getNotifyOn().contains(action)) {
return false;
}
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class);
AlarmAssignee assignee = alarmInfo.getAssignee();
return AlarmAssignmentNotificationInfo.builder()
.action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? "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)
.userName(trigger.getMsg().getMetaData().getValue("userName"))
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_ASSIGNMENT;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN);
}
}

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

@ -0,0 +1,90 @@
/**
* 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 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.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
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.dao.notification.trigger.RuleEngineMsgTrigger;
import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@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()) {
return false;
}
if (triggerConfig.isOnlyUserComments()) {
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
if (comment.getType() == AlarmCommentType.SYSTEM) {
return false;
}
}
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
TbMsg msg = trigger.getMsg();
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class);
AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class);
return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userName(msg.getMetaData().getValue("userName"))
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_COMMENT;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED);
}
}

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

@ -0,0 +1,120 @@
/**
* 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 org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm;
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.NotificationInfo;
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.dao.alarm.AlarmApiCallResult;
import org.thingsboard.server.dao.notification.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@Service
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) {
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate();
Alarm alarm = alarmUpdate.getAlarm();
if (!typeMatches(alarm, triggerConfig)) {
return false;
}
if (alarmUpdate.isCreated()) {
if (triggerConfig.getNotifyOn().contains(AlarmAction.CREATED)) {
return severityMatches(alarm, triggerConfig);
}
} else if (alarmUpdate.isSeverityChanged()) {
if (triggerConfig.getNotifyOn().contains(AlarmAction.SEVERITY_CHANGED)) {
return severityMatches(alarmUpdate.getOld(), triggerConfig) || severityMatches(alarm, triggerConfig);
} else {
// if we haven't yet sent notification about the alarm
return !severityMatches(alarmUpdate.getOld(), triggerConfig) && severityMatches(alarm, triggerConfig);
}
} else if (alarmUpdate.isAcknowledged()) {
if (triggerConfig.getNotifyOn().contains(AlarmAction.ACKNOWLEDGED)) {
return severityMatches(alarm, triggerConfig);
}
} else if (alarmUpdate.isCleared()) {
if (triggerConfig.getNotifyOn().contains(AlarmAction.CLEARED)) {
return severityMatches(alarm, triggerConfig);
}
}
return false;
}
@Override
public boolean matchesClearRule(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) {
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate();
Alarm alarm = alarmUpdate.getAlarm();
if (!typeMatches(alarm, triggerConfig)) {
return false;
}
if (alarmUpdate.isDeleted()) {
return true;
}
ClearRule clearRule = triggerConfig.getClearRule();
if (clearRule != null) {
if (isNotEmpty(clearRule.getAlarmStatuses())) {
return AlarmStatusFilter.from(clearRule.getAlarmStatuses()).matches(alarm);
}
}
return false;
}
private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity());
}
private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType());
}
@Override
public NotificationInfo constructNotificationInfo(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) {
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate();
AlarmInfo alarmInfo = alarmUpdate.getAlarm();
return AlarmNotificationInfo.builder()
.alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType())
.action(alarmUpdate.isCreated() ? "created" :
alarmUpdate.isSeverityChanged() ? "severity changed" :
alarmUpdate.isAcknowledged() ? "acknowledged" :
alarmUpdate.isCleared() ? "cleared" :
alarmUpdate.isDeleted() ? "deleted" : null)
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM;
}
}

76
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java

@ -0,0 +1,76 @@
/**
* 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.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.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.DeviceInactivityNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceInactivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class DeviceInactivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<DeviceInactivityNotificationRuleTriggerConfig> {
private final TbDeviceProfileCache deviceProfileCache;
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) {
DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator();
if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) {
return triggerConfig.getDevices().contains(deviceId.getId());
} else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) {
DeviceProfile deviceProfile = deviceProfileCache.get(TenantId.SYS_TENANT_ID, deviceId);
return deviceProfile != null && triggerConfig.getDeviceProfiles().contains(deviceProfile.getUuidId());
} else {
return true;
}
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) {
TbMsg msg = trigger.getMsg();
return DeviceInactivityNotificationInfo.builder()
.deviceId(msg.getOriginator().getId())
.deviceName(msg.getMetaData().getValue("deviceName"))
.deviceType(msg.getMetaData().getValue("deviceType"))
.deviceLabel(msg.getMetaData().getValue("deviceLabel"))
.deviceCustomerId(msg.getCustomerId())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.DEVICE_INACTIVITY;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.INACTIVITY_EVENT);
}
}

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

@ -0,0 +1,60 @@
/**
* 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.notification.info.EntitiesLimitNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.dao.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@Service
@RequiredArgsConstructor
public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> {
private final TenantService tenantService;
@Override
public boolean matchesFilter(EntitiesLimitTrigger trigger, EntitiesLimitNotificationRuleTriggerConfig triggerConfig) {
if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) {
return false;
}
return (int) (trigger.getLimit() * triggerConfig.getThreshold()) == trigger.getCurrentCount(); // strict comparing not to send notification on each new entity
}
@Override
public NotificationInfo constructNotificationInfo(EntitiesLimitTrigger trigger, EntitiesLimitNotificationRuleTriggerConfig triggerConfig) {
return EntitiesLimitNotificationInfo.builder()
.entityType(trigger.getEntityType())
.currentCount(trigger.getCurrentCount())
.limit(trigger.getLimit())
.percents((int) (((float)trigger.getCurrentCount() / trigger.getLimit()) * 100))
.tenantId(trigger.getTenantId())
.tenantName(tenantService.findTenantById(trigger.getTenantId()).getName())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITIES_LIMIT;
}
}

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

@ -0,0 +1,89 @@
/**
* 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 org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
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.NotificationInfo;
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.TbMsg;
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
String msgType = trigger.getMsg().getType();
if (msgType.equals(DataConstants.ENTITY_CREATED)) {
if (!triggerConfig.isCreated()) {
return false;
}
} else if (msgType.equals(DataConstants.ENTITY_UPDATED)) {
if (!triggerConfig.isUpdated()) {
return false;
}
} else if (msgType.equals(DataConstants.ENTITY_DELETED)) {
if (!triggerConfig.isDeleted()) {
return false;
}
} else {
return false;
}
return triggerConfig.getEntityType() == null || getEntityType(trigger.getMsg()) == triggerConfig.getEntityType();
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
TbMsg msg = trigger.getMsg();
String msgType = msg.getType();
ActionType actionType = msgType.equals(DataConstants.ENTITY_CREATED) ? ActionType.ADDED :
msgType.equals(DataConstants.ENTITY_UPDATED) ? ActionType.UPDATED :
msgType.equals(DataConstants.ENTITY_DELETED) ? ActionType.DELETED : null;
return EntityActionNotificationInfo.builder()
.entityId(msg.getOriginator())
.entityName(msg.getMetaData().getValue("entityName"))
.actionType(actionType)
.originatorUserId(UUID.fromString(msg.getMetaData().getValue("userId")))
.originatorUserName(msg.getMetaData().getValue("userName"))
.entityCustomerId(msg.getCustomerId())
.build();
}
private static EntityType getEntityType(TbMsg msg) {
return Optional.ofNullable(msg.getMetaData().getValue("entityType"))
.map(EntityType::valueOf).orElse(null);
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITY_ACTION;
}
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED);
}
}

56
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.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.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;
@Service
@RequiredArgsConstructor
public class NewPlatformVersionTriggerProcessor implements NotificationRuleTriggerProcessor<NewPlatformVersionTrigger, NewPlatformVersionNotificationRuleTriggerConfig> {
private final PartitionService partitionService;
@Override
public boolean matchesFilter(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) {
// todo: don't send repetitive notification after platform restart?
if (!partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition()) {
return false;
}
return trigger.getMessage().isUpdateAvailable();
}
@Override
public NotificationInfo constructNotificationInfo(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) {
return NewPlatformVersionNotificationInfo.builder()
.message(trigger.getMessage().getMessage())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.NEW_PLATFORM_VERSION;
}
}

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

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.rule.trigger;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
public interface NotificationRuleTriggerProcessor<T extends NotificationRuleTrigger, C extends NotificationRuleTriggerConfig> {
boolean matchesFilter(T trigger, C triggerConfig);
default boolean matchesClearRule(T trigger, C triggerConfig) {
return false;
}
NotificationInfo constructNotificationInfo(T trigger, C triggerConfig);
NotificationRuleTriggerType getTriggerType();
}

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

@ -0,0 +1,98 @@
/**
* 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 org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo;
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.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.notification.trigger.RuleEngineComponentLifecycleEventTrigger;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Set;
@Service
public class RuleEngineComponentLifecycleEventTriggerProcessor implements NotificationRuleTriggerProcessor<RuleEngineComponentLifecycleEventTrigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineComponentLifecycleEventTrigger trigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) {
if (CollectionUtils.isNotEmpty(triggerConfig.getRuleChains())) {
if (!triggerConfig.getRuleChains().contains(trigger.getRuleChainId().getId())) {
return false;
}
}
EntityType componentType = trigger.getComponentId().getEntityType();
Set<ComponentLifecycleEvent> trackedEvents;
boolean onlyFailures;
if (componentType == EntityType.RULE_CHAIN) {
trackedEvents = triggerConfig.getRuleChainEvents();
onlyFailures = triggerConfig.isOnlyRuleChainLifecycleFailures();
} else if (componentType == EntityType.RULE_NODE && triggerConfig.isTrackRuleNodeEvents()) {
trackedEvents = triggerConfig.getRuleNodeEvents();
onlyFailures = triggerConfig.isOnlyRuleNodeLifecycleFailures();
} else {
return false;
}
if (CollectionUtils.isEmpty(trackedEvents)) {
trackedEvents = Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED);
}
if (!trackedEvents.contains(trigger.getEventType())) {
return false;
}
if (onlyFailures) {
return trigger.getError() != null;
}
return true;
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineComponentLifecycleEventTrigger trigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) {
return RuleEngineComponentLifecycleEventNotificationInfo.builder()
.ruleChainId(trigger.getRuleChainId())
.ruleChainName(trigger.getRuleChainName())
.componentId(trigger.getComponentId())
.componentName(trigger.getComponentName())
.action(trigger.getEventType() == ComponentLifecycleEvent.STARTED ? "start" :
trigger.getEventType() == ComponentLifecycleEvent.UPDATED ? "update" :
trigger.getEventType() == ComponentLifecycleEvent.STOPPED ? "stop" : null)
.eventType(trigger.getEventType())
.error(getErrorMsg(trigger.getError()))
.build();
}
private String getErrorMsg(Throwable error) {
if (error == null) return null;
StringWriter sw = new StringWriter();
error.printStackTrace(new PrintWriter(sw));
return StringUtils.abbreviate(ExceptionUtils.getStackTrace(error), 200);
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT;
}
}

27
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java

@ -0,0 +1,27 @@
/**
* 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 org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger;
import java.util.Set;
public interface RuleEngineMsgNotificationRuleTriggerProcessor<C extends NotificationRuleTriggerConfig> extends NotificationRuleTriggerProcessor<RuleEngineMsgTrigger, C> {
Set<String> getSupportedMsgTypes();
}

5
application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java

@ -20,16 +20,17 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -48,6 +49,8 @@ public abstract class AbstractPartitionBasedService<T extends EntityId> extends
protected final ConcurrentMap<TopicPartitionInfo, List<ListenableFuture<?>>> partitionedFetchTasks = new ConcurrentHashMap<>();
final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>();
@Autowired
protected PartitionService partitionService;
protected ListeningScheduledExecutorService scheduledExecutor;
abstract protected String getServiceName();

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

@ -18,17 +18,20 @@ package org.thingsboard.server.service.queue;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
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;
@ -36,9 +39,6 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@ -64,9 +64,11 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@ -74,12 +76,16 @@ import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -122,6 +128,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private final EdgeNotificationService edgeNotificationService;
private final OtaPackageStateService firmwareStateService;
private final GitVersionControlQueueService vcQueueService;
private final NotificationSchedulerService notificationSchedulerService;
private final TbCoreConsumerStats stats;
protected final TbQueueConsumer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
private final TbQueueConsumer<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer;
@ -147,8 +154,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
OtaPackageStateService firmwareStateService,
GitVersionControlQueueService vcQueueService,
PartitionService partitionService,
Optional<JwtSettingsService> jwtSettingsService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
ApplicationEventPublisher eventPublisher,
Optional<JwtSettingsService> jwtSettingsService,
NotificationSchedulerService notificationSchedulerService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();
@ -161,6 +170,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
this.statsService = statsService;
this.firmwareStateService = firmwareStateService;
this.vcQueueService = vcQueueService;
this.notificationSchedulerService = notificationSchedulerService;
}
@PostConstruct
@ -255,6 +265,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
}
callback.onSuccess();
} else if (toCoreMsg.hasNotificationSchedulerServiceMsg()) {
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
@ -337,6 +351,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} else if (toCoreNotification.hasVcResponseMsg()) {
vcQueueService.processResponse(toCoreNotification.getVcResponseMsg());
callback.onSuccess();
} else if (toCoreNotification.hasToSubscriptionMgrMsg()) {
forwardToSubMgrService(toCoreNotification.getToSubscriptionMgrMsg(), callback);
}
if (statsEnabled) {
stats.log(toCoreNotification);
@ -460,6 +476,18 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
localSubscriptionService.onSubscriptionUpdate(msg.getSubUpdate().getSessionId(), TbSubscriptionUtils.fromProto(msg.getSubUpdate()), callback);
} else if (msg.hasAlarmSubUpdate()) {
localSubscriptionService.onSubscriptionUpdate(msg.getAlarmSubUpdate().getSessionId(), TbSubscriptionUtils.fromProto(msg.getAlarmSubUpdate()), callback);
} else if (msg.hasNotificationsSubUpdate()) {
TransportProtos.NotificationsSubscriptionUpdateProto subUpdateProto = msg.getNotificationsSubUpdate();
NotificationsSubscriptionUpdate notificationsSubscriptionUpdate;
if (StringUtils.isNotEmpty(subUpdateProto.getNotificationUpdate())) {
NotificationUpdate notificationUpdate = JacksonUtil.fromString(subUpdateProto.getNotificationUpdate(), NotificationUpdate.class);
notificationsSubscriptionUpdate = new NotificationsSubscriptionUpdate(notificationUpdate);
} else {
NotificationRequestUpdate notificationRequestUpdate = JacksonUtil.fromString(subUpdateProto.getNotificationRequestUpdate(), NotificationRequestUpdate.class);
notificationsSubscriptionUpdate = new NotificationsSubscriptionUpdate(notificationRequestUpdate);
}
localSubscriptionService.onSubscriptionUpdate(subUpdateProto.getSessionId(),
subUpdateProto.getSubscriptionId(), notificationsSubscriptionUpdate, callback);
} else {
throwNotHandled(msg, callback);
}
@ -472,6 +500,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getTelemetrySub()), callback);
} else if (msg.hasAlarmSub()) {
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getAlarmSub()), callback);
} else if (msg.hasNotificationsSub()) {
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getNotificationsSub()), callback);
} else if (msg.hasNotificationsCountSub()) {
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getNotificationsCountSub()), callback);
} else if (msg.hasSubClose()) {
TbSubscriptionCloseProto closeProto = msg.getSubClose();
subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback);
@ -512,6 +544,17 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback);
} else if (msg.hasNotificationUpdate()) {
TransportProtos.NotificationUpdateProto updateProto = msg.getNotificationUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
UserId recipientId = new UserId(new UUID(updateProto.getRecipientIdMSB(), updateProto.getRecipientIdLSB()));
NotificationUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationUpdate.class);
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, callback);
} else if (msg.hasNotificationRequestUpdate()) {
TransportProtos.NotificationRequestUpdateProto updateProto = msg.getNotificationRequestUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
NotificationRequestUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationRequestUpdate.class);
subscriptionManagerService.onNotificationRequestUpdate(tenantId, update, callback);
} else {
throwNotHandled(msg, callback);
}
@ -541,6 +584,17 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
}
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
try {
notificationSchedulerService.scheduleNotificationRequest(tenantId, notificationRequestId, msg.getTs());
callback.onSuccess();
} catch (Exception e) {
callback.onFailure(new RuntimeException("Failed to schedule notification request", e));
}
}
private void forwardToEdgeNotificationService(EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) {
if (statsEnabled) {
stats.log(edgeNotificationMsg);

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

@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue;
import com.google.protobuf.ProtocolStringList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -126,8 +127,9 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbAssetProfileCache assetProfileCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
PartitionService partitionService, ApplicationEventPublisher eventPublisher,
TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
this.statisticsService = statisticsService;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory;

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

@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
@ -75,6 +76,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbAssetProfileCache assetProfileCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService;
@ -83,7 +85,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
PartitionService partitionService, ApplicationEventPublisher eventPublisher,
TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
@ -91,6 +94,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
this.assetProfileCache = assetProfileCache;
this.apiUsageStateService = apiUsageStateService;
this.partitionService = partitionService;
this.eventPublisher = eventPublisher;
this.nfConsumer = nfConsumer;
this.jwtSettingsService = jwtSettingsService;
}
@ -205,6 +209,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
}
}
eventPublisher.publishEvent(componentLifecycleMsg);
}
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
actorContext.tellWithHighPriority(actorMsg);

2
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -79,7 +79,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity;
import org.thingsboard.server.exception.ToErrorResponseEntity;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;

8
application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java

@ -16,10 +16,10 @@
package org.thingsboard.server.service.security;
import com.google.common.util.concurrent.FutureCallback;
import org.thingsboard.server.service.telemetry.exception.AccessDeniedException;
import org.thingsboard.server.service.telemetry.exception.EntityNotFoundException;
import org.thingsboard.server.service.telemetry.exception.InternalErrorException;
import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
import org.thingsboard.server.exception.AccessDeniedException;
import org.thingsboard.server.exception.EntityNotFoundException;
import org.thingsboard.server.exception.InternalErrorException;
import org.thingsboard.server.exception.UnauthorizedException;
/**
* Created by ashvayka on 31.03.18.

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java

@ -18,7 +18,7 @@ package org.thingsboard.server.service.security.auth.mfa.provider.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.CollectionsUtil;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.model.mfa.account.BackupCodeTwoFaAccountConfig;

20
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -17,7 +17,9 @@ package org.thingsboard.server.service.security.permission;
import org.thingsboard.server.common.data.EntityType;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
public enum Resource {
ADMIN_SETTINGS(),
@ -43,25 +45,27 @@ public enum Resource {
EDGE(EntityType.EDGE),
RPC(EntityType.RPC),
QUEUE(EntityType.QUEUE),
VERSION_CONTROL;
VERSION_CONTROL,
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE);
private final EntityType entityType;
private final Set<EntityType> entityTypes;
Resource() {
this.entityType = null;
this.entityTypes = Collections.emptySet();
}
Resource(EntityType entityType) {
this.entityType = entityType;
Resource(EntityType... entityTypes) {
this.entityTypes = Set.of(entityTypes);
}
public Optional<EntityType> getEntityType() {
return Optional.ofNullable(entityType);
public Set<EntityType> getEntityTypes() {
return entityTypes;
}
public static Resource of(EntityType entityType) {
for (Resource resource : Resource.values()) {
if (resource.getEntityType().orElse(null) == entityType) {
if (resource.getEntityTypes().contains(entityType)) {
return resource;
}
}

1
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -40,6 +40,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);
put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION, systemEntityPermissionChecker);
}
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -49,6 +49,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.RPC, tenantEntityPermissionChecker);
put(Resource.QUEUE, queuePermissionChecker);
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

153
application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java

@ -0,0 +1,153 @@
/**
* 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.slack;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.slack.api.Slack;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiRequest;
import com.slack.api.methods.SlackApiTextResponse;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.request.conversations.ConversationsListRequest;
import com.slack.api.methods.request.users.UsersListRequest;
import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType;
import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class DefaultSlackService implements SlackService {
private final NotificationSettingsService notificationSettingsService;
private final Slack slack = Slack.getInstance();
private final Cache<String, List<SlackConversation>> cache = Caffeine.newBuilder()
.expireAfterWrite(20, TimeUnit.SECONDS)
.maximumSize(100)
.build();
private static final int CONVERSATIONS_LOAD_LIMIT = 1000;
@Override
public void sendMessage(TenantId tenantId, String token, String conversationId, String message) {
ChatPostMessageRequest request = ChatPostMessageRequest.builder()
.channel(conversationId)
.text(message)
.build();
sendRequest(token, request, MethodsClient::chatPostMessage);
}
@Override
public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType) {
return cache.get(conversationType + ":" + token, k -> {
if (conversationType == SlackConversationType.DIRECT) {
UsersListRequest request = UsersListRequest.builder()
.limit(CONVERSATIONS_LOAD_LIMIT)
.build();
UsersListResponse response = sendRequest(token, request, MethodsClient::usersList);
return response.getMembers().stream()
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot())
.map(user -> {
SlackConversation conversation = new SlackConversation();
conversation.setId(user.getId());
conversation.setName(String.format("@%s (%s)", user.getName(), user.getRealName()));
return conversation;
})
.collect(Collectors.toList());
} else {
ConversationsListRequest request = ConversationsListRequest.builder()
.types(List.of(conversationType == SlackConversationType.PUBLIC_CHANNEL ?
ConversationType.PUBLIC_CHANNEL :
ConversationType.PRIVATE_CHANNEL))
.limit(CONVERSATIONS_LOAD_LIMIT)
.excludeArchived(true)
.build();
ConversationsListResponse response = sendRequest(token, request, MethodsClient::conversationsList);
return response.getChannels().stream()
.filter(channel -> !channel.isArchived())
.map(channel -> {
SlackConversation conversation = new SlackConversation();
conversation.setId(channel.getId());
conversation.setName("#" + channel.getName());
return conversation;
})
.collect(Collectors.toList());
}
});
}
@Override
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) {
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType);
return conversations.stream()
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getName(), namePattern))
.findFirst().orElse(null);
}
@Override
public String getToken(TenantId tenantId) {
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig)
settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK);
if (slackConfig != null) {
return slackConfig.getBotToken();
} else {
return null;
}
}
private <T extends SlackApiRequest, R extends SlackApiTextResponse> R sendRequest(String token, T request, ThrowingBiFunction<MethodsClient, T, R> method) {
MethodsClient client = slack.methods(token);
R response;
try {
response = method.apply(client, request);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
if (!response.isOk()) {
String error = response.getError();
if (error == null) {
error = "unknown error";
} else if (error.contains("missing_scope")) {
String neededScope = response.getNeeded();
error = "bot token scope '" + neededScope + "' is needed";
}
throw new RuntimeException("Failed to send message via Slack: " + error);
}
return response;
}
}

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

@ -28,6 +28,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -138,6 +139,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private static final List<EntityKey> PERSISTENT_ENTITY_FIELDS = Arrays.asList(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "type"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "label"),
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"));
private final TenantService tenantService;
@ -149,7 +151,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private final TbServiceInfoProvider serviceInfoProvider;
private final EntityQueryRepository entityQueryRepository;
private final DbTypeInfoComponent dbTypeInfoComponent;
@Autowired @Lazy
private TelemetrySubscriptionService tsSubService;
@Value("${state.defaultInactivityTimeoutInSec}")
@ -196,11 +198,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
this.dbTypeInfoComponent = dbTypeInfoComponent;
}
@Autowired
public void setTsSubService(TelemetrySubscriptionService tsSubService) {
this.tsSubService = tsSubService;
}
@PostConstruct
public void init() {
super.init();
@ -333,6 +330,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
md.putValue("deviceLabel", device.getLabel());
md.putValue("deviceType", device.getType());
stateData.setMetaData(md);
callback.onSuccess();
@ -585,6 +583,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", device.getName());
md.putValue("deviceLabel", device.getLabel());
md.putValue("deviceType", device.getType());
DeviceStateData deviceStateData = DeviceStateData.builder()
.customerId(device.getCustomerId())
@ -664,6 +663,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
.build();
TbMsgMetaData md = new TbMsgMetaData();
md.putValue("deviceName", getEntryValue(ed, EntityKeyType.ENTITY_FIELD, "name", ""));
md.putValue("deviceLabel", getEntryValue(ed, EntityKeyType.ENTITY_FIELD, "label", ""));
md.putValue("deviceType", getEntryValue(ed, EntityKeyType.ENTITY_FIELD, "type", ""));
return DeviceStateData.builder()
.customerId(deviceIdInfo.getCustomerId())

93
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -15,8 +15,8 @@
*/
package org.thingsboard.server.service.subscription;
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.common.util.JacksonUtil;
@ -25,11 +25,11 @@ import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto;
@ -61,8 +60,11 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -83,34 +85,18 @@ import java.util.function.Predicate;
@Slf4j
@TbCoreComponent
@Service
@RequiredArgsConstructor
public class DefaultSubscriptionManagerService extends TbApplicationEventListener<PartitionChangeEvent> implements SubscriptionManagerService {
@Autowired
private AttributesService attrService;
@Autowired
private TimeseriesService tsService;
@Autowired
private NotificationsTopicService notificationsTopicService;
@Autowired
private PartitionService partitionService;
@Autowired
private TbServiceInfoProvider serviceInfoProvider;
@Autowired
private TbQueueProducerProvider producerProvider;
@Autowired
private TbLocalSubscriptionService localSubscriptionService;
@Autowired
private DeviceStateService deviceStateService;
@Autowired
private TbClusterService clusterService;
private final AttributesService attrService;
private final TimeseriesService tsService;
private final NotificationsTopicService notificationsTopicService;
private final PartitionService partitionService;
private final TbServiceInfoProvider serviceInfoProvider;
private final TbQueueProducerProvider producerProvider;
private final TbLocalSubscriptionService localSubscriptionService;
private final DeviceStateService deviceStateService;
private final TbClusterService clusterService;
private final Map<EntityId, Set<TbSubscription>> subscriptionsByEntityId = new ConcurrentHashMap<>();
private final Map<String, Map<Integer, TbSubscription>> subscriptionsByWsSessionId = new ConcurrentHashMap<>();
@ -325,6 +311,51 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
callback.onSuccess();
}
@Override
public void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate, TbCallback callback) {
Set<TbSubscription> subscriptions = subscriptionsByEntityId.get(recipientId);
if (subscriptions != null) {
NotificationsSubscriptionUpdate subscriptionUpdate = new NotificationsSubscriptionUpdate(notificationUpdate);
subscriptions.stream()
.filter(subscription -> subscription.getType() == TbSubscriptionType.NOTIFICATIONS
|| subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)
.forEach(subscription -> {
if (serviceId.equals(subscription.getServiceId())) {
localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(),
subscription.getSubscriptionId(), subscriptionUpdate, TbCallback.EMPTY);
} else {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId());
ToCoreNotificationMsg updateProto = TbSubscriptionUtils.notificationsSubUpdateToProto(subscription, subscriptionUpdate);
TbProtoQueueMsg<ToCoreNotificationMsg> queueMsg = new TbProtoQueueMsg<>(subscription.getEntityId().getId(), updateProto);
toCoreNotificationsProducer.send(tpi, queueMsg, null);
}
});
}
callback.onSuccess();
}
@Override
public void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate, TbCallback callback) {
NotificationsSubscriptionUpdate subscriptionUpdate = new NotificationsSubscriptionUpdate(notificationRequestUpdate);
subscriptionsByEntityId.forEach((entityId, subscriptions) -> {
if (entityId.getEntityType() != EntityType.USER) {
return;
}
subscriptions.forEach(subscription -> {
if (subscription.getType() != TbSubscriptionType.NOTIFICATIONS &&
subscription.getType() != TbSubscriptionType.NOTIFICATIONS_COUNT) {
return;
}
if (!subscription.getTenantId().equals(tenantId) || !subscription.getServiceId().equals(serviceId)) {
return;
}
localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(), subscription.getSubscriptionId(),
subscriptionUpdate, TbCallback.EMPTY);
});
});
callback.onSuccess();
}
@Override
public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List<String> keys, boolean notifyDevice, TbCallback callback) {
onLocalTelemetrySubUpdate(entityId,

45
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -49,22 +49,21 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.GetTsCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -96,7 +95,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
@Autowired
private TelemetryWebSocketService wsService;
private WebSocketService wsService;
@Autowired
private EntityService entityService;
@ -167,7 +166,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
@Override
public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) {
public void handleCmd(WebSocketSessionRef session, EntityDataCmd cmd) {
TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx != null) {
log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
@ -358,7 +357,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
@Override
public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) {
public void handleCmd(WebSocketSessionRef session, EntityCountCmd cmd) {
TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx == null) {
ctx = createSubCtx(session, cmd);
@ -378,7 +377,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
@Override
public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) {
public void handleCmd(WebSocketSessionRef session, AlarmDataCmd cmd) {
TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx == null) {
log.debug("[{}][{}] Creating new alarm subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
@ -469,7 +468,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
}
private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
private TbEntityDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
@ -480,7 +479,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
return ctx;
}
private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
private TbEntityCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId());
@ -492,7 +491,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
private TbAlarmDataSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription,

31
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -21,18 +21,19 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@ -152,7 +153,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
break;
}
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update));
}
callback.onSuccess();
}
@ -163,7 +164,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
TbSubscription subscription = subscriptionsBySessionId
.getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) {
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update));
}
callback.onSuccess();
}
@Override
public void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback) {
TbSubscription subscription = subscriptionsBySessionId.getOrDefault(sessionId, Collections.emptyMap()).get(subscriptionId);
if (subscription != null && (subscription.getType() == TbSubscriptionType.NOTIFICATIONS
|| subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)) {
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update));
}
callback.onSuccess();
}

2
application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java

@ -17,7 +17,7 @@ package org.thingsboard.server.service.subscription;
import lombok.Data;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey;
@Data
public class ReadTsKvQueryInfo {

2
application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java → application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.sub;
package org.thingsboard.server.service.subscription;
public enum SubscriptionErrorCode {

6
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java

@ -20,11 +20,14 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import java.util.List;
@ -48,5 +51,8 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback);
void onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate, TbCallback callback);
void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate, TbCallback callback);
}

14
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -29,9 +29,9 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Arrays;
@ -50,10 +50,10 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
@Getter
protected PageData<EntityData> data;
public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService,
public TbAbstractDataSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats,
TelemetryWebSocketSessionRef sessionRef, int cmdId) {
WebSocketSessionRef sessionRef, int cmdId) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.subToEntityIdMap = new ConcurrentHashMap<>();
}
@ -185,7 +185,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId())
.updateConsumer((s, subscriptionUpdate) -> sendWsMsg(s, subscriptionUpdate, keysType))
.updateProcessor((sub, subscriptionUpdate) -> sendWsMsg(sub.getSessionId(), subscriptionUpdate, keysType))
.allKeys(false)
.keyStates(keyStates)
.scope(scope)
@ -219,7 +219,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId())
.updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
.updateProcessor((sub, subscriptionUpdate) -> sendWsMsg(sub.getSessionId(), subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
.allKeys(false)
.keyStates(keyStates)
.latestValues(latestValues)

19
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
@ -39,10 +38,10 @@ import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.HashMap;
@ -64,11 +63,11 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
protected final Lock wsLock = new ReentrantLock(true);
protected final String serviceId;
protected final SubscriptionServiceStatistics stats;
private final TelemetryWebSocketService wsService;
private final WebSocketService wsService;
protected final EntityService entityService;
protected final TbLocalSubscriptionService localSubscriptionService;
protected final AttributesService attributesService;
protected final TelemetryWebSocketSessionRef sessionRef;
protected final WebSocketSessionRef sessionRef;
protected final int cmdId;
protected final Set<Integer> subToDynamicValueKeySet;
@Getter
@ -80,10 +79,10 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
protected volatile ScheduledFuture<?> refreshTask;
protected volatile boolean stopped;
public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService,
public TbAbstractSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats,
TelemetryWebSocketSessionRef sessionRef, int cmdId) {
WebSocketSessionRef sessionRef, int cmdId) {
this.serviceId = serviceId;
this.wsService = wsService;
this.entityService = entityService;
@ -142,7 +141,7 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap))
.updateProcessor((subscription, subscriptionUpdate) -> dynamicValueSubUpdate(subscription.getSessionId(), subscriptionUpdate, dynamicValueKeySubMap))
.allKeys(false)
.keyStates(keyStates)
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE)

16
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -41,11 +41,11 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Collection;
@ -82,10 +82,10 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
private int alarmInvocationAttempts;
public TbAlarmDataSubCtx(String serviceId, TelemetryWebSocketService wsService,
public TbAlarmDataSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService,
TelemetryWebSocketSessionRef sessionRef, int cmdId,
WebSocketSessionRef sessionRef, int cmdId,
int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription;
@ -176,7 +176,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId())
.updateConsumer(this::sendWsMsg)
.updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update))
.ts(startTs)
.build();
localSubscriptionService.addSubscription(subscription);

9
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java

@ -17,13 +17,10 @@ package org.thingsboard.server.service.subscription;
import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import java.util.List;
import java.util.function.BiConsumer;
public class TbAlarmsSubscription extends TbSubscription<AlarmSubscriptionUpdate> {
@ -33,8 +30,8 @@ public class TbAlarmsSubscription extends TbSubscription<AlarmSubscriptionUpdate
@Builder
public TbAlarmsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<String, AlarmSubscriptionUpdate> updateConsumer, long ts) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateConsumer);
BiConsumer<TbSubscription<AlarmSubscriptionUpdate>, AlarmSubscriptionUpdate> updateProcessor, long ts) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateProcessor);
this.ts = ts;
}

6
application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java

@ -19,7 +19,7 @@ import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.Map;
import java.util.function.BiConsumer;
@ -32,9 +32,9 @@ public class TbAttributeSubscription extends TbSubscription<TelemetrySubscriptio
@Builder
public TbAttributeSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<String, TelemetrySubscriptionUpdate> updateConsumer,
BiConsumer<TbSubscription<TelemetrySubscriptionUpdate>, TelemetrySubscriptionUpdate> updateProcessor,
boolean allKeys, Map<String, Long> keyStates, TbAttributeSubscriptionScope scope) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateConsumer);
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateProcessor);
this.allKeys = allKeys;
this.keyStates = keyStates;
this.scope = scope;

10
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java

@ -19,18 +19,18 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
@Slf4j
public class TbEntityCountSubCtx extends TbAbstractSubCtx<EntityCountQuery> {
private volatile int result;
public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService,
public TbEntityCountSubCtx(String serviceId, WebSocketService wsService, EntityService entityService,
TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService,
SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) {
SubscriptionServiceStatistics stats, WebSocketSessionRef sessionRef, int cmdId) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
}

18
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -28,13 +28,13 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.Collections;
@ -59,9 +59,9 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
private final int maxEntitiesPerDataSubscription;
private Map<EntityId, Map<String, TsValue>> latestTsEntityData;
public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService,
public TbEntityDataSubCtx(String serviceId, WebSocketService wsService, EntityService entityService,
TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService,
SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerDataSubscription) {
SubscriptionServiceStatistics stats, WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerDataSubscription) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.maxEntitiesPerDataSubscription = maxEntitiesPerDataSubscription;
}

17
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java

@ -15,20 +15,19 @@
*/
package org.thingsboard.server.service.subscription;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
public interface TbEntityDataSubscriptionService {
void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd);
void handleCmd(WebSocketSessionRef sessionId, EntityDataCmd cmd);
void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd);
void handleCmd(WebSocketSessionRef sessionId, EntityCountCmd cmd);
void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd);
void handleCmd(WebSocketSessionRef sessionId, AlarmDataCmd cmd);
void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId);

7
application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java

@ -18,8 +18,9 @@ package org.thingsboard.server.service.subscription;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
public interface TbLocalSubscriptionService {
@ -33,6 +34,8 @@ public interface TbLocalSubscriptionService {
void onSubscriptionUpdate(String sessionId, AlarmSubscriptionUpdate update, TbCallback callback);
void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback);
void onApplicationEvent(PartitionChangeEvent event);
void onApplicationEvent(ClusterTopologyChangeEvent event);

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java

@ -33,7 +33,7 @@ public abstract class TbSubscription<T> {
private final TenantId tenantId;
private final EntityId entityId;
private final TbSubscriptionType type;
private final BiConsumer<String, T> updateConsumer;
private final BiConsumer<? extends TbSubscription<T>, T> updateProcessor;
@Override
public boolean equals(Object o) {

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.service.subscription;
public enum TbSubscriptionType {
TIMESERIES, ATTRIBUTES, ALARMS
TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS, NOTIFICATIONS_COUNT
}

92
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@ -51,10 +52,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeletePr
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto;
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.HashMap;
@ -107,6 +113,17 @@ public class TbSubscriptionUtils {
.setTs(alarmSub.getTs());
msgBuilder.setAlarmSub(alarmSubProto.build());
break;
case NOTIFICATIONS:
NotificationsSubscription notificationsSub = (NotificationsSubscription) subscription;
msgBuilder.setNotificationsSub(TransportProtos.NotificationsSubscriptionProto.newBuilder()
.setSub(subscriptionProto)
.setLimit(notificationsSub.getLimit()));
break;
case NOTIFICATIONS_COUNT:
NotificationsCountSubscription notificationsCountSub = (NotificationsCountSubscription) subscription;
msgBuilder.setNotificationsCountSub(TransportProtos.NotificationsCountSubscriptionProto.newBuilder()
.setSub(subscriptionProto));
break;
}
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
}
@ -168,6 +185,29 @@ public class TbSubscriptionUtils {
return builder.build();
}
public static NotificationsSubscription fromProto(TransportProtos.NotificationsSubscriptionProto notificationsSub) {
TbSubscriptionProto sub = notificationsSub.getSub();
return NotificationsSubscription.builder()
.serviceId(sub.getServiceId())
.sessionId(sub.getSessionId())
.subscriptionId(sub.getSubscriptionId())
.tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB())))
.entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB())))
.limit(notificationsSub.getLimit())
.build();
}
public static NotificationsCountSubscription fromProto(TransportProtos.NotificationsCountSubscriptionProto notificationsCountSub) {
TbSubscriptionProto sub = notificationsCountSub.getSub();
return NotificationsCountSubscription.builder()
.serviceId(sub.getServiceId())
.sessionId(sub.getSessionId())
.subscriptionId(sub.getSubscriptionId())
.tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB())))
.entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB())))
.build();
}
public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) {
if (proto.getErrorCode() > 0) {
return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
@ -343,4 +383,50 @@ public class TbSubscriptionUtils {
msgBuilder.setAlarmDelete(builder);
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
}
public static ToCoreNotificationMsg notificationsSubUpdateToProto(TbSubscription subscription, NotificationsSubscriptionUpdate update) {
TransportProtos.NotificationsSubscriptionUpdateProto.Builder updateProto = TransportProtos.NotificationsSubscriptionUpdateProto.newBuilder()
.setSessionId(subscription.getSessionId())
.setSubscriptionId(subscription.getSubscriptionId());
if (update.getNotificationUpdate() != null) {
updateProto.setNotificationUpdate(JacksonUtil.toString(update.getNotificationUpdate()));
}
if (update.getNotificationRequestUpdate() != null) {
updateProto.setNotificationRequestUpdate(JacksonUtil.toString(update.getNotificationRequestUpdate()));
}
return ToCoreNotificationMsg.newBuilder()
.setToLocalSubscriptionServiceMsg(TransportProtos.LocalSubscriptionServiceMsgProto.newBuilder()
.setNotificationsSubUpdate(updateProto)
.build())
.build();
}
public static ToCoreMsg notificationUpdateToProto(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate) {
TransportProtos.NotificationUpdateProto updateProto = TransportProtos.NotificationUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRecipientIdMSB(recipientId.getId().getMostSignificantBits())
.setRecipientIdLSB(recipientId.getId().getLeastSignificantBits())
.setUpdate(JacksonUtil.toString(notificationUpdate))
.build();
return ToCoreMsg.newBuilder()
.setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder()
.setNotificationUpdate(updateProto)
.build())
.build();
}
public static ToCoreNotificationMsg notificationRequestUpdateToProto(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate) {
TransportProtos.NotificationRequestUpdateProto updateProto = TransportProtos.NotificationRequestUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setUpdate(JacksonUtil.toString(notificationRequestUpdate))
.build();
return ToCoreNotificationMsg.newBuilder()
.setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder()
.setNotificationRequestUpdate(updateProto)
.build())
.build();
}
}

6
application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java

@ -19,7 +19,7 @@ import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.Map;
import java.util.function.BiConsumer;
@ -39,9 +39,9 @@ public class TbTimeseriesSubscription extends TbSubscription<TelemetrySubscripti
@Builder
public TbTimeseriesSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<String, TelemetrySubscriptionUpdate> updateConsumer,
BiConsumer<TbSubscription<TelemetrySubscriptionUpdate>, TelemetrySubscriptionUpdate> updateProcessor,
boolean allKeys, Map<String, Long> keyStates, long startTime, long endTime, boolean latestValues) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer);
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateProcessor);
this.allKeys = allKeys;
this.keyStates = keyStates;
this.startTime = startTime;

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

@ -26,13 +26,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ThrowingRunnable;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
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.service.apiusage.RateLimitService;
import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService;
@ -72,7 +73,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkEntityExportLimit(ctx.getTenantId())) {
if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_EXPORT)) {
throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
@ -84,7 +85,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException {
if (!rateLimitService.checkEntityImportLimit(ctx.getTenantId())) {
if (!rateLimitService.checkRateLimit(ctx.getTenantId(), LimitedApi.ENTITY_IMPORT)) {
throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
if (exportData.getEntity() == null || exportData.getEntity().getId() == null) {

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

@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.id.HasId;
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.sync.ThrowingRunnable;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;

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

Loading…
Cancel
Save