diff --git a/application/pom.xml b/application/pom.xml
index b653465050..3093dcfb57 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -350,6 +350,10 @@
org.jboss.aerogear
aerogear-otp-java
+
+ com.slack.api
+ slack-api-client
+
diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql
index 949b4229de..9bb85e3447 100644
--- a/application/src/main/data/upgrade/3.4.4/schema_update.sql
+++ b/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
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index eb83d3e301..c0fff5e387 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/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
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index 9d774e26f4..59d633540e 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/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();
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
index 6fc8d5d3d8..da45f0f53d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
+++ b/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 {
+public class RuleChainActor extends RuleEngineComponentActor {
private final RuleChain ruleChain;
@@ -101,6 +100,16 @@ public class RuleChainActor extends ComponentActor> extends ComponentActor {
+
+ 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();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
index 560aafd033..e8dec535f6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
+++ b/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 {
+public class RuleNodeActor extends RuleEngineComponentActor {
private final String ruleChainName;
private final RuleChainId ruleChainId;
@@ -133,6 +132,16 @@ public class RuleNodeActor extends ComponentActor {
+ 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 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 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 & HasTenantId, I extends EntityId> E checkEntityId(I entityId, ThrowingBiFunction 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 & 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 > void logEntityAction(SecurityUser user, EntityType entityType, E savedEntity, ActionType actionType) {
+ logEntityAction(user, entityType, null, savedEntity, actionType, null);
+ }
+
+ protected > 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 doSaveAndLog(EntityType entityType, E entity, BiFunction 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 , I extends EntityId> void doDeleteAndLog(EntityType entityType, E entity, BiConsumer 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;
}
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
index 1d429e9157..3fdc1bc236 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
+++ b/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(
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
new file mode 100644
index 0000000000..24daab29f0
--- /dev/null
+++ b/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 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 processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream()
+ .collect(Collectors.toMap(m -> m, deliveryMethod -> {
+ Map 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 recipientsPreview = new LinkedHashSet<>();
+ Map recipientsCountByTarget = new HashMap<>();
+ List 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 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 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 getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
+ accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ);
+ return notificationCenter.getAvailableDeliveryMethods(user.getTenantId());
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
new file mode 100644
index 0000000000..4fdb0136f3
--- /dev/null
+++ b/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 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);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
new file mode 100644
index 0000000000..3107435e2d
--- /dev/null
+++ b/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 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 getNotificationTargetsByIds(@RequestParam("ids") UUID[] ids,
+ @AuthenticationPrincipal SecurityUser user) {
+ // generic permission
+ List 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 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 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("");
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
new file mode 100644
index 0000000000..5524255648
--- /dev/null
+++ b/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: Hello everyone\",\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 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 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);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
index 1d7c7dbde8..916228147e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
index f74040e498..ac3277a5a8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
index 6c46fc0fdf..a28842a6ee 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
+++ b/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 getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) {
+ return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
+ }
+
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index e57bb73262..06963e466f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index f43607b24c..200a6c71d2 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/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 internalSessionMap = new ConcurrentHashMap<>();
private final ConcurrentMap 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 blacklistedSessions = new ConcurrentHashMap<>();
+ private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>();
private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap> 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> 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);
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java b/application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java
rename to application/src/main/java/org/thingsboard/server/exception/AccessDeniedException.java
index eaf3b3159e..a82d4b6234 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/AccessDeniedException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java b/application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java
rename to application/src/main/java/org/thingsboard/server/exception/EntityNotFoundException.java
index 31f38eb50f..53fd323691 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/EntityNotFoundException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java b/application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java
rename to application/src/main/java/org/thingsboard/server/exception/InternalErrorException.java
index 30a4c462b6..ffa0d0cb07 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InternalErrorException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java b/application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java
rename to application/src/main/java/org/thingsboard/server/exception/InvalidParametersException.java
index 335b7d47d2..ad41e75b3d 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/InvalidParametersException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java b/application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java
similarity index 93%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java
rename to application/src/main/java/org/thingsboard/server/exception/ToErrorResponseEntity.java
index 48b3468cd4..ce79f852f3 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/ToErrorResponseEntity.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java b/application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java
rename to application/src/main/java/org/thingsboard/server/exception/UnauthorizedException.java
index d7ca828085..2d3d8dbf8f 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UnauthorizedException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java b/application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java
similarity index 95%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java
rename to application/src/main/java/org/thingsboard/server/exception/UncheckedApiException.java
index f404d1a563..38f8a4a51f 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/exception/UncheckedApiException.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index 257db58a9b..0531f3e7e9 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/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
diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
index 5674517ee1..2eedd18d00 100644
--- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
+++ b/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) {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
index 5ba092937f..f845634aa0 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
+++ b/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 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) -> {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java
similarity index 66%
rename from application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
rename to application/src/main/java/org/thingsboard/server/service/apiusage/limits/DefaultRateLimitService.java
index dd3f4c5150..e995a8bd1f 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
+++ b/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> rateLimits = new ConcurrentHashMap<>();
+ private final Map> 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 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 rateLimits = this.rateLimits.get(rateLimitsKey);
+ Map 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)) {
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/LimitedApi.java
new file mode 100644
index 0000000000..f69ece6661
--- /dev/null
+++ b/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 configExtractor;
+
+ public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) {
+ return configExtractor.apply(profileConfiguration);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java
similarity index 81%
rename from application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
rename to application/src/main/java/org/thingsboard/server/service/apiusage/limits/RateLimitService.java
index af5debac2e..c984ec8fa5 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
+++ b/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);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
index 0d53c9bf9c..e26499aacc 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
+++ b/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 {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
index 3156ff404b..11ff6a3321 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
+++ b/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 removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
index 03a0671162..fe4f761f81 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
+++ b/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));
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java
index 3765358c6d..c5f8f85831 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
index 0e289f73f3..6f86632ffd 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
index e452a766a4..6905e669eb 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
+++ b/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();
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/NotificationExecutorService.java
new file mode 100644
index 0000000000..81ad36a4a1
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 33e8dc7eea..a182a4d83f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/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 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;
+ }
+ }
+ }
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index fa5c4d1971..fb6b28592c 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/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();
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index 43d6a4df0a..13943d5698 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
new file mode 100644
index 0000000000..65469a65a0
--- /dev/null
+++ b/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 {
+
+ 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 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 targets = notificationRequest.getTargets().stream().map(NotificationTargetId::new)
+ .map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)).collect(Collectors.toList());
+ Set 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> results = new ArrayList<>();
+
+ for (NotificationTarget target : targets) {
+ List> 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> 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 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> results = new ArrayList<>();
+ if (!deliveryMethods.isEmpty()) {
+ for (NotificationRecipient recipient : recipients) {
+ for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
+ ListenableFuture 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 processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) {
+ if (ctx.getStats().contains(deliveryMethod, recipient.getId())) {
+ return Futures.immediateFailedFuture(new AlreadySentException());
+ }
+ Map 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 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 getAvailableDeliveryMethods(TenantId tenantId) {
+ Set 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 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 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 channels, NotificationCenter webNotificationChannel) {
+ this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c));
+ this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java
new file mode 100644
index 0000000000..25790d8b5e
--- /dev/null
+++ b/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 implements NotificationSchedulerService {
+
+ private final NotificationCenter notificationCenter;
+ private final NotificationRequestService notificationRequestService;
+ private final SchedulerComponent scheduler;
+ private final NotificationExecutorService notificationExecutor;
+
+ private final Map scheduledNotificationRequests = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void init() {
+ super.init();
+ }
+
+ @Override
+ protected Map>> onAddedPartitions(Set addedPartitions) {
+ PageDataIterable 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 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
new file mode 100644
index 0000000000..5492650d48
--- /dev/null
+++ b/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 templates;
+ @Getter
+ private Set 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 getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) {
+ return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod);
+ }
+
+ public T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map 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 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 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 createTemplateContext(User recipient) {
+ Map 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;
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java b/application/src/main/java/org/thingsboard/server/service/notification/NotificationSchedulerService.java
new file mode 100644
index 0000000000..c02d96345d
--- /dev/null
+++ b/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);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
new file mode 100644
index 0000000000..ce9c6e3b98
--- /dev/null
+++ b/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 {
+
+ private final MailService mailService;
+ private final MailExecutorService executor;
+
+ @Override
+ public ListenableFuture 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/NotificationChannel.java
new file mode 100644
index 0000000000..7e82d31c27
--- /dev/null
+++ b/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 {
+
+ ListenableFuture sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx);
+
+ NotificationDeliveryMethod getDeliveryMethod();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
new file mode 100644
index 0000000000..67108c2a37
--- /dev/null
+++ b/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 {
+
+ private final SlackService slackService;
+ private final ExternalCallExecutorService executor;
+
+ @Override
+ public ListenableFuture 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java b/application/src/main/java/org/thingsboard/server/service/notification/channels/SmsNotificationChannel.java
new file mode 100644
index 0000000000..b49f1a9b73
--- /dev/null
+++ b/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 {
+
+ private final SmsService smsService;
+ private final SmsExecutorService executor;
+
+ @Override
+ public ListenableFuture 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java
new file mode 100644
index 0000000000..da61aa0c30
--- /dev/null
+++ b/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 triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
+
+ private final Map ruleEngineMsgTypeToTriggerType = new HashMap<>();
+
+ @Override
+ public void process(TenantId tenantId, NotificationRuleTrigger trigger) {
+ List 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 notificationRequests = notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(tenantId, rule.getId(), trigger.getOriginatorEntityId());
+ if (notificationRequests.isEmpty()) {
+ return;
+ }
+
+ List 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 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 scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId);
+ for (NotificationRequestId notificationRequestId : scheduledForRule) {
+ notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId);
+ }
+ });
+ }
+
+ @Autowired
+ public void setTriggerProcessors(Collection processors) {
+ processors.forEach(processor -> {
+ triggerProcessors.put(processor.getTriggerType(), processor);
+ if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) {
+ Set supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor>) processor).getSupportedMsgTypes();
+ supportedMsgTypes.forEach(supportedMsgType -> {
+ ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType());
+ });
+ }
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
new file mode 100644
index 0000000000..9fd0c06118
--- /dev/null
+++ b/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 {
+
+ @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 getSupportedMsgTypes() {
+ return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
new file mode 100644
index 0000000000..0250f0929f
--- /dev/null
+++ b/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 {
+
+ @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 getSupportedMsgTypes() {
+ return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
new file mode 100644
index 0000000000..085fb02bbc
--- /dev/null
+++ b/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 {
+
+ @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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java
new file mode 100644
index 0000000000..a9d932f3ec
--- /dev/null
+++ b/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 {
+
+ 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 getSupportedMsgTypes() {
+ return Set.of(DataConstants.INACTIVITY_EVENT);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
new file mode 100644
index 0000000000..3b15a56f8b
--- /dev/null
+++ b/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 {
+
+ 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
new file mode 100644
index 0000000000..1fb6270ea5
--- /dev/null
+++ b/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 {
+
+ @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 getSupportedMsgTypes() {
+ return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NewPlatformVersionTriggerProcessor.java
new file mode 100644
index 0000000000..ba346caab8
--- /dev/null
+++ b/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 {
+
+ 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java
new file mode 100644
index 0000000000..bb24f66d87
--- /dev/null
+++ b/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 {
+
+ boolean matchesFilter(T trigger, C triggerConfig);
+
+ default boolean matchesClearRule(T trigger, C triggerConfig) {
+ return false;
+ }
+
+ NotificationInfo constructNotificationInfo(T trigger, C triggerConfig);
+
+ NotificationRuleTriggerType getTriggerType();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
new file mode 100644
index 0000000000..afda3cc001
--- /dev/null
+++ b/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 {
+
+ @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 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineMsgNotificationRuleTriggerProcessor.java
new file mode 100644
index 0000000000..4436b4737f
--- /dev/null
+++ b/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 extends NotificationRuleTriggerProcessor {
+
+ Set getSupportedMsgTypes();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java b/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java
index 72554f1516..bda4108ea7 100644
--- a/application/src/main/java/org/thingsboard/server/service/partition/AbstractPartitionBasedService.java
+++ b/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 extends
protected final ConcurrentMap>> partitionedFetchTasks = new ConcurrentHashMap<>();
final Queue> subscribeQueue = new ConcurrentLinkedQueue<>();
+ @Autowired
+ protected PartitionService partitionService;
protected ListeningScheduledExecutorService scheduledExecutor;
abstract protected String getServiceName();
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index b55b965330..571d4d6e96 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/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> usageStatsConsumer;
private final TbQueueConsumer> firmwareStatesConsumer;
@@ -147,8 +154,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService jwtSettingsService) {
- super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
+ ApplicationEventPublisher eventPublisher,
+ Optional 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> nfConsumer;
protected final Optional jwtSettingsService;
@@ -83,7 +85,8 @@ public abstract class AbstractConsumerService> nfConsumer, Optional jwtSettingsService) {
+ PartitionService partitionService, ApplicationEventPublisher eventPublisher,
+ TbQueueConsumer> nfConsumer, Optional jwtSettingsService) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
@@ -91,6 +94,7 @@ public abstract class AbstractConsumerService 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 getEntityType() {
- return Optional.ofNullable(entityType);
+ public Set 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;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
index e03c5da5b7..4169aa1c2e 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
+++ b/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() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
index a27e0b674f..cb924cabca 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
+++ b/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() {
diff --git a/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java b/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
new file mode 100644
index 0000000000..f5bdcb7373
--- /dev/null
+++ b/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> 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 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 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 R sendRequest(String token, T request, ThrowingBiFunction 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;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
index 7f38386dc7..60c159360b 100644
--- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
+++ b/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 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 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> subscriptionsByEntityId = new ConcurrentHashMap<>();
private final Map> 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 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 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 keys, boolean notifyDevice, TbCallback callback) {
onLocalTelemetrySubUpdate(entityId,
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
index 7d76ac9825..9fa8429f78 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
+++ b/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> 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 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 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 sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription,
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
index ea2ac714e6..64f1de8662 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
+++ b/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();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java
index a194ba0267..4cfd218893 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java
+++ b/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 {
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java
similarity index 96%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
rename to application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionErrorCode.java
index 1527fd8e64..999a447d7a 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionErrorCode.java
+++ b/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 {
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
index 410b2a4a1c..1db4348828 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
+++ b/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 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 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 sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
+ .updateProcessor((sub, subscriptionUpdate) -> sendWsMsg(sub.getSessionId(), subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
.allKeys(false)
.keyStates(keyStates)
.latestValues(latestValues)
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
index e0b432559c..d6b3052613 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
+++ b/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 {
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 subToDynamicValueKeySet;
@Getter
@@ -80,10 +79,10 @@ public abstract class TbAbstractSubCtx {
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 {
.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)
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
index c90302998a..ccba16fbd8 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
+++ b/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 {
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 {
.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);
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java
index 7ecd66ea62..bc23d1a9b6 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmsSubscription.java
+++ b/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 {
@@ -33,8 +30,8 @@ public class TbAlarmsSubscription extends TbSubscription updateConsumer, long ts) {
- super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateConsumer);
+ BiConsumer, AlarmSubscriptionUpdate> updateProcessor, long ts) {
+ super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateProcessor);
this.ts = ts;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java
index 6aa145d6c8..3746758caa 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAttributeSubscription.java
+++ b/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 updateConsumer,
+ BiConsumer, TelemetrySubscriptionUpdate> updateProcessor,
boolean allKeys, Map 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;
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java
index 440cb97136..eac94cde7d 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java
+++ b/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 {
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);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
index 42eceb77bc..924d30d5c9 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
+++ b/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 {
private final int maxEntitiesPerDataSubscription;
private Map> 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;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
index d8362339f1..cd6921282e 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java
index 6a59402cae..516eb20684 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java
+++ b/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);
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
index 5d7e89bd8a..dfad6a41de 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java
@@ -33,7 +33,7 @@ public abstract class TbSubscription {
private final TenantId tenantId;
private final EntityId entityId;
private final TbSubscriptionType type;
- private final BiConsumer updateConsumer;
+ private final BiConsumer extends TbSubscription, T> updateProcessor;
@Override
public boolean equals(Object o) {
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
index 2149814436..cd560bbbaf 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
+++ b/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
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
index c30ac9f0d5..914c8a7c55 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
+++ b/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();
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
index a48d3985ee..400cf07e7e 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
+++ b/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 updateConsumer,
+ BiConsumer, TelemetrySubscriptionUpdate> updateProcessor,
boolean allKeys, Map 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;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
index 85c8ee0f8e..b646cb7ef6 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
+++ b/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 , I extends EntityId> EntityExportData 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 , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData 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) {
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
index e19b21d70d..8ffd4796fe 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
+++ b/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;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
index 905fa53e91..785dd50a92 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
@@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
@@ -64,7 +64,6 @@ import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.ClearRepositoryGitRequest;
import org.thingsboard.server.service.sync.vc.data.CommitGitRequest;
-import org.thingsboard.server.service.sync.vc.data.ContentsDiffGitRequest;
import org.thingsboard.server.service.sync.vc.data.EntitiesContentGitRequest;
import org.thingsboard.server.service.sync.vc.data.EntityContentGitRequest;
import org.thingsboard.server.service.sync.vc.data.ListBranchesGitRequest;
@@ -78,11 +77,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
index 3a7fb50a61..8bbb8e2498 100644
--- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java
@@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
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.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
new file mode 100644
index 0000000000..35db65bab2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
@@ -0,0 +1,216 @@
+/**
+ * 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.system;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.protobuf.ProtocolStringList;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.ApiUsageState;
+import org.thingsboard.server.common.data.FeaturesInfo;
+import org.thingsboard.server.common.data.SystemInfo;
+import org.thingsboard.server.common.data.SystemInfoData;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.JsonDataEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.queue.ServiceType;
+import org.thingsboard.server.common.stats.TbApiUsageStateClient;
+import org.thingsboard.server.dao.oauth2.OAuth2Service;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
+import org.thingsboard.server.queue.discovery.DiscoveryService;
+import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
+import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
+import static org.thingsboard.common.util.SystemUtil.getFreeDiscSpace;
+import static org.thingsboard.common.util.SystemUtil.getFreeMemory;
+import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
+import static org.thingsboard.common.util.SystemUtil.getTotalCpuUsage;
+import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
+import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
+
+@TbCoreComponent
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultSystemInfoService extends TbApplicationEventListener implements SystemInfoService {
+
+ public static final FutureCallback CALLBACK = new FutureCallback<>() {
+ @Override
+ public void onSuccess(@Nullable Integer result) {
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ log.warn("Failed to persist system info", t);
+ }
+ };
+
+ private final TbServiceInfoProvider serviceInfoProvider;
+ private final PartitionService partitionService;
+ private final DiscoveryService discoveryService;
+ private final TelemetrySubscriptionService telemetryService;
+ private final TbApiUsageStateClient apiUsageStateClient;
+ private final AdminSettingsService adminSettingsService;
+ private final OAuth2Service oAuth2Service;
+ private volatile ScheduledExecutorService scheduler;
+
+ @Override
+ protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
+ if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
+ boolean myPartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition();
+ synchronized (this) {
+ if (myPartition) {
+ if (scheduler == null) {
+ scheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("tb-system-info-scheduler"));
+ scheduler.scheduleAtFixedRate(this::saveCurrentSystemInfo, 0, 1, TimeUnit.MINUTES);
+ }
+ } else {
+ destroy();
+ }
+ }
+ }
+ }
+
+ @Override
+ public SystemInfo getSystemInfo() {
+ SystemInfo systemInfo = new SystemInfo();
+
+ ServiceInfo serviceInfo = serviceInfoProvider.getServiceInfoWithCurrentSystemInfo();
+
+ if (discoveryService.isMonolith()) {
+ systemInfo.setMonolith(true);
+ systemInfo.setSystemData(Collections.singletonList(createSystemInfoData(serviceInfo)));
+ } else {
+ systemInfo.setSystemData(getSystemData(serviceInfo));
+ }
+
+ return systemInfo;
+ }
+
+ protected void saveCurrentSystemInfo() {
+ if (discoveryService.isMonolith()) {
+ saveCurrentMonolithSystemInfo();
+ } else {
+ saveCurrentClusterSystemInfo();
+ }
+ }
+
+ @Override
+ public FeaturesInfo getFeaturesInfo() {
+ FeaturesInfo featuresInfo = new FeaturesInfo();
+ featuresInfo.setEmailEnabled(isEmailEnabled());
+ featuresInfo.setSmsEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null);
+ featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled());
+ featuresInfo.setTwoFaEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings") != null);
+ featuresInfo.setNotificationEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications") != null);
+ return featuresInfo;
+ }
+
+ private boolean isEmailEnabled() {
+ AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
+ if (mailSettings != null) {
+ JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
+ if (mailFrom != null) {
+ return DataValidator.doValidateEmail(mailFrom.asText());
+ }
+ }
+ return false;
+ }
+
+ private void saveCurrentClusterSystemInfo() {
+ long ts = System.currentTimeMillis();
+ List clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfoWithCurrentSystemInfo());
+ BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
+ doSave(Collections.singletonList(clusterDataKv));
+ }
+
+ private void saveCurrentMonolithSystemInfo() {
+ long ts = System.currentTimeMillis();
+ List tsList = new ArrayList<>();
+
+ getMemoryUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", v))));
+ getTotalMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalMemory", v))));
+ getFreeMemory().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("freeMemory", v))));
+ getCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new DoubleDataEntry("cpuUsage", v))));
+ getTotalCpuUsage().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new DoubleDataEntry("totalCpuUsage", v))));
+ getFreeDiscSpace().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("freeDiscSpace", v))));
+ getTotalDiscSpace().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("totalDiscSpace", v))));
+
+ doSave(tsList);
+ }
+
+ private void doSave(List telemetry) {
+ ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID);
+ telemetryService.saveAndNotifyInternal(TenantId.SYS_TENANT_ID, apiUsageState.getId(), telemetry, CALLBACK);
+ }
+
+ private List getSystemData(ServiceInfo serviceInfo) {
+ List clusterSystemData = new ArrayList<>();
+ clusterSystemData.add(createSystemInfoData(serviceInfo));
+ this.discoveryService.getOtherServers()
+ .stream()
+ .map(this::createSystemInfoData)
+ .forEach(clusterSystemData::add);
+ return clusterSystemData;
+ }
+
+ private SystemInfoData createSystemInfoData(ServiceInfo serviceInfo) {
+ ProtocolStringList serviceTypes = serviceInfo.getServiceTypesList();
+ SystemInfoData infoData = new SystemInfoData();
+ infoData.setServiceId(serviceInfo.getServiceId());
+ infoData.setServiceType(serviceTypes.size() > 1 ? "MONOLITH" : serviceTypes.get(0));
+ infoData.setMemoryUsage(serviceInfo.getSystemInfo().getMemoryUsage());
+ infoData.setTotalMemory(serviceInfo.getSystemInfo().getTotalMemory());
+ infoData.setFreeMemory(serviceInfo.getSystemInfo().getFreeMemory());
+ infoData.setCpuUsage(serviceInfo.getSystemInfo().getCpuUsage());
+ infoData.setTotalCpuUsage(serviceInfo.getSystemInfo().getTotalCpuUsage());
+ infoData.setFreeDiscSpace(serviceInfo.getSystemInfo().getFreeDiscSpace());
+ infoData.setTotalDiscSpace(serviceInfo.getSystemInfo().getTotalDiscSpace());
+ return infoData;
+ }
+
+ @PreDestroy
+ private void destroy() {
+ if (scheduler != null) {
+ scheduler.shutdownNow();
+ scheduler = null;
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java
new file mode 100644
index 0000000000..1ca0fb8fe0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.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.system;
+
+import org.thingsboard.server.common.data.FeaturesInfo;
+import org.thingsboard.server.common.data.SystemInfo;
+
+public interface SystemInfoService {
+ SystemInfo getSystemInfo();
+
+ FeaturesInfo getFeaturesInfo();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java
index 95f0c58e57..32efaa6346 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java
@@ -20,13 +20,17 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
-import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
+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.cluster.TbClusterService;
+import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import javax.annotation.Nullable;
@@ -38,33 +42,26 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
+import java.util.function.Supplier;
/**
* Created by ashvayka on 27.03.18.
*/
@Slf4j
-public abstract class AbstractSubscriptionService extends TbApplicationEventListener{
+public abstract class AbstractSubscriptionService extends TbApplicationEventListener {
protected final Set currentPartitions = ConcurrentHashMap.newKeySet();
- protected final TbClusterService clusterService;
- protected final PartitionService partitionService;
+ @Autowired
+ protected TbClusterService clusterService;
+ @Autowired
+ protected PartitionService partitionService;
+ @Autowired
protected Optional subscriptionManagerService;
protected ExecutorService wsCallBackExecutor;
- public AbstractSubscriptionService(TbClusterService clusterService,
- PartitionService partitionService) {
- this.clusterService = clusterService;
- this.partitionService = partitionService;
- }
-
- @Autowired(required = false)
- public void setSubscriptionManagerService(Optional subscriptionManagerService) {
- this.subscriptionManagerService = subscriptionManagerService;
- }
-
- abstract String getExecutorPrefix();
+ protected abstract String getExecutorPrefix();
@PostConstruct
public void initExecutor() {
@@ -86,6 +83,22 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList
}
}
+ protected void forwardToSubscriptionManagerService(TenantId tenantId, EntityId entityId,
+ Consumer toSubscriptionManagerService,
+ Supplier toCore) {
+ TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
+ if (currentPartitions.contains(tpi)) {
+ if (subscriptionManagerService.isPresent()) {
+ toSubscriptionManagerService.accept(subscriptionManagerService.get());
+ } else {
+ log.warn("Possible misconfiguration because subscriptionManagerService is null!");
+ }
+ } else {
+ TransportProtos.ToCoreMsg toCoreMsg = toCore.get();
+ clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
+ }
+ }
+
protected void addWsCallback(ListenableFuture saveFuture, Consumer callback) {
Futures.addCallback(saveFuture, new FutureCallback() {
@Override
@@ -98,4 +111,5 @@ public abstract class AbstractSubscriptionService extends TbApplicationEventList
}
}, wsCallBackExecutor);
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
index 6eb5f16b27..5956d2b2dd 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
@@ -19,15 +19,16 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
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;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
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.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
@@ -35,7 +36,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
-import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@@ -44,56 +45,35 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
-import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
-import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmApiCallResult;
-import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService;
-import org.thingsboard.server.gen.transport.TransportProtos;
-import org.thingsboard.server.queue.discovery.PartitionService;
+import org.thingsboard.server.dao.notification.NotificationRuleProcessingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
-import org.thingsboard.server.cluster.TbClusterService;
-import org.thingsboard.server.service.subscription.SubscriptionManagerService;
+import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
+import org.thingsboard.server.dao.notification.trigger.AlarmTrigger;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import java.util.Collection;
-import java.util.Optional;
/**
* Created by ashvayka on 27.03.18.
*/
@Service
@Slf4j
+@RequiredArgsConstructor
public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService {
private final AlarmService alarmService;
- private final AlarmCommentService alarmCommentService;
+ private final TbAlarmCommentService alarmCommentService;
private final TbApiUsageReportClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService;
-
- public DefaultAlarmSubscriptionService(TbClusterService clusterService,
- PartitionService partitionService,
- AlarmService alarmService,
- TbApiUsageReportClient apiUsageClient,
- TbApiUsageStateService apiUsageStateService,
- AlarmCommentService alarmCommentService) {
- super(clusterService, partitionService);
- this.alarmService = alarmService;
- this.apiUsageClient = apiUsageClient;
- this.apiUsageStateService = apiUsageStateService;
- this.alarmCommentService = alarmCommentService;
- }
-
- @Autowired(required = false)
- public void setSubscriptionManagerService(Optional subscriptionManagerService) {
- this.subscriptionManagerService = subscriptionManagerService;
- }
+ private final NotificationRuleProcessingService notificationRuleProcessingService;
@Override
- String getExecutorPrefix() {
+ protected String getExecutorPrefix() {
return "alarm";
}
@@ -144,7 +124,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity())))
.build();
- alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
+ try {
+ alarmCommentService.saveAlarmComment(alarm, alarmComment, null);
+ } catch (ThingsboardException e) {
+ log.error("Failed to save alarm comment", e);
+ }
}
}
if (result.isCreated()) {
@@ -228,20 +212,14 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
@Deprecated
private void onAlarmUpdated(AlarmOperationResult result) {
wsCallBackExecutor.submit(() -> {
- Alarm alarm = result.getAlarm();
+ AlarmInfo alarm = new AlarmInfo(result.getAlarm());
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, new AlarmInfo(alarm), TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, new AlarmInfo(alarm));
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
+ });
}
});
}
@@ -251,18 +229,15 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
AlarmInfo alarm = result.getAlarm();
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
+ });
}
+ notificationRuleProcessingService.process(tenantId, AlarmTrigger.builder()
+ .alarmUpdate(result)
+ .build());
});
}
@@ -271,18 +246,15 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
AlarmInfo alarm = result.getAlarm();
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmDeletedProto(tenantId, entityId, alarm);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onAlarmDeleted(tenantId, entityId, alarm, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toAlarmDeletedProto(tenantId, entityId, alarm);
+ });
}
+ notificationRuleProcessingService.process(tenantId, AlarmTrigger.builder()
+ .alarmUpdate(result)
+ .build());
});
}
@@ -315,7 +287,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
if (request != null && request.getUserId() != null) {
alarmComment.userId(request.getUserId());
}
- alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment.build());
+ try {
+ alarmCommentService.saveAlarmComment(alarm, alarmComment.build(), null);
+ } catch (ThingsboardException e) {
+ log.error("Failed to save alarm comment", e);
+ }
}
}
return result;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 2d0f0c04c0..ffa6d7bf48 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -40,13 +40,11 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
-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.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
-import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.entityview.TbEntityViewService;
@@ -85,11 +83,8 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
public DefaultTelemetrySubscriptionService(AttributesService attrService,
TimeseriesService tsService,
@Lazy TbEntityViewService tbEntityViewService,
- TbClusterService clusterService,
- PartitionService partitionService,
TbApiUsageReportClient apiUsageClient,
TbApiUsageStateService apiUsageStateService) {
- super(clusterService, partitionService);
this.attrService = attrService;
this.tsService = tsService;
this.tbEntityViewService = tbEntityViewService;
@@ -375,73 +370,49 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toAttributesUpdateProto(tenantId, entityId, scope, attributes);
+ });
}
private void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onAttributesDelete(tenantId, entityId, scope, keys, notifyDevice, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toAttributesDeleteProto(tenantId, entityId, scope, keys, notifyDevice);
+ });
}
private void onTimeSeriesUpdate(TenantId tenantId, EntityId entityId, List ts) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onTimeSeriesUpdate(tenantId, entityId, ts, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toTimeseriesUpdateProto(tenantId, entityId, ts);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ subscriptionManagerService.onTimeSeriesUpdate(tenantId, entityId, ts, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toTimeseriesUpdateProto(tenantId, entityId, ts);
+ });
}
private void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, List ts) {
- TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
- if (currentPartitions.contains(tpi)) {
- if (subscriptionManagerService.isPresent()) {
- List updated = new ArrayList<>();
- List deleted = new ArrayList<>();
-
- ts.stream().filter(Objects::nonNull).forEach(res -> {
- if (res.isRemoved()) {
- if (res.getData() != null) {
- updated.add(res.getData());
- } else {
- deleted.add(res.getKey());
- }
+ forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
+ List updated = new ArrayList<>();
+ List deleted = new ArrayList<>();
+
+ ts.stream().filter(Objects::nonNull).forEach(res -> {
+ if (res.isRemoved()) {
+ if (res.getData() != null) {
+ updated.add(res.getData());
+ } else {
+ deleted.add(res.getKey());
}
- });
+ }
+ });
- subscriptionManagerService.get().onTimeSeriesUpdate(tenantId, entityId, updated, TbCallback.EMPTY);
- subscriptionManagerService.get().onTimeSeriesDelete(tenantId, entityId, deleted, TbCallback.EMPTY);
- } else {
- log.warn("Possible misconfiguration because subscriptionManagerService is null!");
- }
- } else {
- TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toTimeseriesDeleteProto(tenantId, entityId, keys);
- clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null);
- }
+ subscriptionManagerService.onTimeSeriesUpdate(tenantId, entityId, updated, TbCallback.EMPTY);
+ subscriptionManagerService.onTimeSeriesDelete(tenantId, entityId, deleted, TbCallback.EMPTY);
+ }, () -> {
+ return TbSubscriptionUtils.toTimeseriesDeleteProto(tenantId, entityId, keys);
+ });
}
private void addVoidCallback(ListenableFuture saveFuture, final FutureCallback callback) {
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
index 75ce35b549..301376b16a 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/InternalTelemetryService.java
@@ -41,6 +41,4 @@ public interface InternalTelemetryService extends RuleEngineTelemetryService {
void deleteLatestInternal(TenantId tenantId, EntityId entityId, List keys, FutureCallback callback);
-
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
new file mode 100644
index 0000000000..65b04ef0ab
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ttl/NotificationsCleanUpService.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ttl;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
+import org.thingsboard.server.dao.notification.NotificationRequestDao;
+import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
+import org.thingsboard.server.queue.discovery.PartitionService;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NOTIFICATION_TABLE_NAME;
+
+@Service
+@ConditionalOnExpression("${sql.ttl.notifications.enabled:true} && ${sql.ttl.notifications.ttl:0} > 0")
+@Slf4j
+public class NotificationsCleanUpService extends AbstractCleanUpService {
+
+ private final SqlPartitioningRepository partitioningRepository;
+ private final NotificationRequestDao notificationRequestDao;
+
+ @Value("${sql.ttl.notifications.ttl:2592000}")
+ private long ttlInSec;
+ @Value("${sql.notifications.partition_size:168}")
+ private int partitionSizeInHours;
+
+ public NotificationsCleanUpService(PartitionService partitionService, SqlPartitioningRepository partitioningRepository,
+ NotificationRequestDao notificationRequestDao) {
+ super(partitionService);
+ this.partitioningRepository = partitioningRepository;
+ this.notificationRequestDao = notificationRequestDao;
+ }
+
+ @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.notifications.checking_interval_ms:86400000})}",
+ fixedDelayString = "${sql.ttl.notifications.checking_interval_ms:86400000}")
+ public void cleanUp() {
+ long expTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec);
+ long partitionDurationMs = TimeUnit.HOURS.toMillis(partitionSizeInHours);
+ if (!isSystemTenantPartitionMine()) {
+ partitioningRepository.cleanupPartitionsCache(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
+ return;
+ }
+
+ long lastRemovedNotificationTs = partitioningRepository.dropPartitionsBefore(NOTIFICATION_TABLE_NAME, expTime, partitionDurationMs);
+ if (lastRemovedNotificationTs > 0) {
+ long gap = TimeUnit.MINUTES.toMillis(10);
+ long requestExpTime = lastRemovedNotificationTs - TimeUnit.SECONDS.toMillis(NotificationRequestConfig.MAX_SENDING_DELAY) - gap;
+ // TODO: double-check this
+ notificationRequestDao.removeAllByCreatedTimeBefore(requestExpTime);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
index ac3bfc497a..d9f49ae4d3 100644
--- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
@@ -19,11 +19,15 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.UpdateMessage;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger;
+import org.thingsboard.server.dao.notification.NotificationRuleProcessingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import javax.annotation.PostConstruct;
@@ -53,6 +57,9 @@ public class DefaultUpdateService implements UpdateService {
@Value("${updates.enabled}")
private boolean updatesEnabled;
+ @Autowired
+ private NotificationRuleProcessingService notificationRuleProcessingService;
+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("tb-update-service"));
private ScheduledFuture checkUpdatesFuture = null;
@@ -66,7 +73,7 @@ public class DefaultUpdateService implements UpdateService {
@PostConstruct
private void init() {
- updateMessage = new UpdateMessage("", false);
+ updateMessage = new UpdateMessage("", false, "");
if (updatesEnabled) {
try {
platform = System.getProperty("platform", "unknown");
@@ -121,11 +128,18 @@ public class DefaultUpdateService implements UpdateService {
request.put(PLATFORM_PARAM, platform);
request.put(VERSION_PARAM, version);
request.put(INSTANCE_ID_PARAM, instanceId.toString());
- JsonNode response = restClient.postForObject(UPDATE_SERVER_BASE_URL+"/api/thingsboard/updates", request, JsonNode.class);
+ JsonNode response = restClient.postForObject(UPDATE_SERVER_BASE_URL + "/api/thingsboard/updates", request, JsonNode.class);
+ UpdateMessage prevUpdateMessage = updateMessage;
updateMessage = new UpdateMessage(
response.get("message").asText(),
- response.get("updateAvailable").asBoolean()
+ response.get("updateAvailable").asBoolean(),
+ version
);
+ if (updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) {
+ notificationRuleProcessingService.process(TenantId.SYS_TENANT_ID, NewPlatformVersionTrigger.builder()
+ .message(updateMessage)
+ .build());
+ }
} catch (Exception e) {
log.trace(e.getMessage());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
similarity index 79%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
rename to application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
index ffde6192ec..258af9d2e4 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.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;
+package org.thingsboard.server.service.ws;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Function;
@@ -21,8 +21,8 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
@@ -48,6 +48,7 @@ import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.util.TenantRateLimitException;
+import org.thingsboard.server.exception.UnauthorizedException;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator;
@@ -56,26 +57,28 @@ import org.thingsboard.server.service.security.ValidationResult;
import org.thingsboard.server.service.security.ValidationResultCode;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
import org.thingsboard.server.service.subscription.TbAttributeSubscription;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbTimeseriesSubscription;
-import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
-import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd;
-import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd;
-import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd;
-import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd;
-import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
-import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
-import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
-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.UnsubscribeCmd;
-import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
-import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
-import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
+import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler;
+import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
+import org.thingsboard.server.service.ws.notification.cmd.WsCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.GetHistoryCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.SubscriptionCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.TelemetryPluginCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
+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.UnsubscribeCmd;
+import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@@ -97,6 +100,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -106,7 +110,8 @@ import java.util.stream.Collectors;
@Service
@TbCoreComponent
@Slf4j
-public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
+@RequiredArgsConstructor
+public class DefaultWebSocketService implements WebSocketService {
public static final int NUMBER_OF_PING_ATTEMPTS = 3;
@@ -121,29 +126,15 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>();
- @Autowired
- private TbLocalSubscriptionService oldSubService;
-
- @Autowired
- private TbEntityDataSubscriptionService entityDataSubService;
-
- @Autowired
- private TelemetryWebSocketMsgEndpoint msgEndpoint;
-
- @Autowired
- private AccessValidator accessValidator;
-
- @Autowired
- private AttributesService attributesService;
-
- @Autowired
- private TimeseriesService tsService;
-
- @Autowired
- private TbServiceInfoProvider serviceInfoProvider;
-
- @Autowired
- private TbTenantProfileCache tenantProfileCache;
+ private final TbLocalSubscriptionService oldSubService;
+ private final TbEntityDataSubscriptionService entityDataSubService;
+ private final NotificationCommandsHandler notificationCmdsHandler;
+ private final WebSocketMsgEndpoint msgEndpoint;
+ private final AccessValidator accessValidator;
+ private final AttributesService attributesService;
+ private final TimeseriesService tsService;
+ private final TbServiceInfoProvider serviceInfoProvider;
+ private final TbTenantProfileCache tenantProfileCache;
@Value("${server.ws.ping_timeout:30000}")
private long pingTimeout;
@@ -154,17 +145,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private ExecutorService executor;
+ private ScheduledExecutorService pingExecutor;
private String serviceId;
- private ScheduledExecutorService pingExecutor;
+ private List> telemetryCmdsHandlers;
+ private List> notificationCmdsHandlers;
@PostConstruct
- public void initExecutor() {
+ public void init() {
serviceId = serviceInfoProvider.getServiceId();
executor = ThingsBoardExecutors.newWorkStealingPool(50, getClass());
pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping"));
pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS);
+
+ telemetryCmdsHandlers = List.of(
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getAttrSubCmds, this::handleWsAttributesSubscriptionCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getTsSubCmds, this::handleWsTimeseriesSubscriptionCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getHistoryCmds, this::handleWsHistoryCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
+ newCmdsHandler(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd)
+ );
+ notificationCmdsHandlers = List.of(
+ newCmdHandler(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd),
+ newCmdHandler(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd),
+ newCmdHandler(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd),
+ newCmdHandler(NotificationCmdsWrapper::getMarkAllAsReadCmd, notificationCmdsHandler::handleMarkAllAsReadCmd),
+ newCmdHandler(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd)
+ );
}
@PreDestroy
@@ -179,7 +192,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
@Override
- public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
+ public void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent event) {
String sessionId = sessionRef.getSessionId();
log.debug(PROCESSING_MSG, sessionId, event);
switch (event.getEventType()) {
@@ -199,49 +212,19 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
@Override
- public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) {
+ public void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg) {
if (log.isTraceEnabled()) {
log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
}
try {
- TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.OBJECT_MAPPER.readValue(msg, TelemetryPluginCmdsWrapper.class);
- if (cmdsWrapper != null) {
- if (cmdsWrapper.getAttrSubCmds() != null) {
- cmdsWrapper.getAttrSubCmds().forEach(cmd -> {
- if (processSubscription(sessionRef, cmd)) {
- handleWsAttributesSubscriptionCmd(sessionRef, cmd);
- }
- });
- }
- if (cmdsWrapper.getTsSubCmds() != null) {
- cmdsWrapper.getTsSubCmds().forEach(cmd -> {
- if (processSubscription(sessionRef, cmd)) {
- handleWsTimeseriesSubscriptionCmd(sessionRef, cmd);
- }
- });
- }
- if (cmdsWrapper.getHistoryCmds() != null) {
- cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getEntityDataCmds() != null) {
- cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getAlarmDataCmds() != null) {
- cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getEntityCountCmds() != null) {
- cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
- cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) {
- cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
- }
- if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) {
- cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
- }
+ switch (sessionRef.getSessionType()) {
+ case TELEMETRY:
+ processTelemetryCmds(sessionRef, msg);
+ break;
+ case NOTIFICATIONS:
+ processNotificationCmds(sessionRef, msg);
+ break;
}
} catch (IOException e) {
log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@@ -249,7 +232,38 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
+ private void processTelemetryCmds(WebSocketSessionRef sessionRef, String msg) throws JsonProcessingException {
+ TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, TelemetryPluginCmdsWrapper.class);
+ if (cmdsWrapper == null) {
+ return;
+ }
+ for (WsCmdListHandler cmdHandler : telemetryCmdsHandlers) {
+ List> cmds = cmdHandler.extractCmds(cmdsWrapper);
+ if (cmds != null) {
+ cmdHandler.handle(sessionRef, cmds);
+ }
+ }
+ }
+
+ private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException {
+ NotificationCmdsWrapper cmdsWrapper = JacksonUtil.fromString(msg, NotificationCmdsWrapper.class);
+ for (WsCmdHandler cmdHandler : notificationCmdsHandlers) {
+ WsCmd cmd = cmdHandler.extractCmd(cmdsWrapper);
+ if (cmd != null) {
+ String sessionId = sessionRef.getSessionId();
+ if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
+ try {
+ cmdHandler.handle(sessionRef, cmd);
+ } catch (Exception e) {
+ log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId,
+ sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e);
+ }
+ }
+ }
+ }
+ }
+
+ private void handleWsEntityDataCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -259,7 +273,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
+ private void handleWsEntityCountCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -269,7 +283,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
+ private void handleWsAlarmDataCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -279,7 +293,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
+ private void handleWsDataUnsubscribeCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -317,7 +331,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void processSessionClose(TelemetryWebSocketSessionRef sessionRef) {
+ private void processSessionClose(WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration != null) {
String sessionId = "[" + sessionRef.getSessionId() + "]";
@@ -351,7 +365,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private boolean processSubscription(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ private boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) return true;
@@ -423,7 +437,11 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
- private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+ private void handleWsAttributesSubscriptionCmd(WebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+ if (!processSubscription(sessionRef, cmd)) {
+ return;
+ }
+
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -444,7 +462,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ private void handleWsAttributesSubscriptionByKeys(WebSocketSessionRef sessionRef,
AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId,
List keys) {
FutureCallback> callback = new FutureCallback<>() {
@@ -468,10 +486,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.allKeys(false)
.keyStates(subState)
.scope(scope)
- .updateConsumer((sessionId, update) -> {
+ .updateProcessor((subscription, update) -> {
subLock.lock();
try {
- sendWsMsg(sessionId, update);
+ sendWsMsg(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@@ -510,7 +528,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
+ private void handleWsHistoryCmd(WebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
String sessionId = sessionRef.getSessionId();
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
@@ -560,7 +578,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
on(r -> Futures.addCallback(tsService.findAll(sessionRef.getSecurityCtx().getTenantId(), entityId, queries), callback, executor), callback::onFailure));
}
- private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ private void handleWsAttributesSubscription(WebSocketSessionRef sessionRef,
AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
FutureCallback> callback = new FutureCallback<>() {
@Override
@@ -581,10 +599,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.entityId(entityId)
.allKeys(true)
.keyStates(subState)
- .updateConsumer((sessionId, update) -> {
+ .updateProcessor((subscription, update) -> {
subLock.lock();
try {
- sendWsMsg(sessionId, update);
+ sendWsMsg(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
@@ -618,7 +636,11 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+ private void handleWsTimeseriesSubscriptionCmd(WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+ if (!processSubscription(sessionRef, cmd)) {
+ return;
+ }
+
String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -638,7 +660,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+ private void handleWsTimeseriesSubscriptionByKeys(WebSocketSessionRef sessionRef,
TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
long startTs;
if (cmd.getTimeWindow() > 0) {
@@ -662,7 +684,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef,
+ private void handleWsTimeseriesSubscription(WebSocketSessionRef sessionRef,
TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
FutureCallback> callback = new FutureCallback>() {
@Override
@@ -677,16 +699,17 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
- .updateConsumer((sessionId, update) -> {
+ .updateProcessor((subscription, update) -> {
subLock.lock();
try {
- sendWsMsg(sessionId, update);
+ sendWsMsg(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
})
.allKeys(true)
- .keyStates(subState).build();
+ .keyStates(subState)
+ .build();
subLock.lock();
try {
@@ -714,7 +737,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure));
}
- private FutureCallback> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) {
+ private FutureCallback> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List keys) {
return new FutureCallback<>() {
@Override
public void onSuccess(List data) {
@@ -729,16 +752,17 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
.subscriptionId(cmd.getCmdId())
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
- .updateConsumer((sessionId, update) -> {
+ .updateProcessor((subscription, update) -> {
subLock.lock();
try {
- sendWsMsg(sessionId, update);
+ sendWsMsg(subscription.getSessionId(), update);
} finally {
subLock.unlock();
}
})
.allKeys(false)
- .keyStates(subState).build();
+ .keyStates(subState)
+ .build();
subLock.lock();
try{
@@ -763,7 +787,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
};
}
- private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ private void unsubscribe(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
oldSubService.cancelAllSessionSubscriptions(sessionId);
} else {
@@ -771,7 +795,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
+ private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
@@ -786,7 +810,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
- private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
+ private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, EntityCountCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
@@ -800,7 +824,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
- private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
+ private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
if (cmd.getCmdId() < 0) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Cmd id is negative value!");
@@ -815,7 +839,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
- private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+ private boolean validateSubscriptionCmd(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Device id is empty!");
@@ -825,11 +849,11 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return true;
}
- private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+ private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId);
}
- private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) {
+ private boolean validateSessionMetadata(WebSocketSessionRef sessionRef, int cmdId, String sessionId) {
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
if (sessionMD == null) {
log.warn("[{}] Session meta data not found. ", sessionId);
@@ -842,15 +866,15 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
}
}
- private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) {
+ private void sendWsMsg(WebSocketSessionRef sessionRef, EntityDataUpdate update) {
sendWsMsg(sessionRef, update.getCmdId(), update);
}
- private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) {
+ private void sendWsMsg(WebSocketSessionRef sessionRef, TelemetrySubscriptionUpdate update) {
sendWsMsg(sessionRef, update.getSubscriptionId(), update);
}
- private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) {
+ private void sendWsMsg(WebSocketSessionRef sessionRef, int cmdId, Object update) {
try {
String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update);
executor.submit(() -> {
@@ -994,9 +1018,52 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
return limit == 0 ? DEFAULT_LIMIT : limit;
}
- private DefaultTenantProfileConfiguration getTenantProfileConfiguration(TelemetryWebSocketSessionRef sessionRef) {
+ private DefaultTenantProfileConfiguration getTenantProfileConfiguration(WebSocketSessionRef sessionRef) {
return Optional.ofNullable(tenantProfileCache.get(sessionRef.getSecurityCtx().getTenantId()))
.map(TenantProfile::getDefaultProfileConfiguration).orElse(null);
}
+
+ public static WsCmdHandler newCmdHandler(java.util.function.Function cmdExtractor,
+ BiConsumer handler) {
+ return new WsCmdHandler<>(cmdExtractor, handler);
+ }
+
+ public static WsCmdListHandler newCmdsHandler(java.util.function.Function> cmdsExtractor,
+ BiConsumer handler) {
+ return new WsCmdListHandler<>(cmdsExtractor, handler);
+ }
+
+ @RequiredArgsConstructor
+ public static class WsCmdHandler {
+ private final java.util.function.Function cmdExtractor;
+ private final BiConsumer handler;
+
+ public C extractCmd(W cmdsWrapper) {
+ return cmdExtractor.apply(cmdsWrapper);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void handle(WebSocketSessionRef sessionRef, Object cmd) {
+ handler.accept(sessionRef, (C) cmd);
+ }
+ }
+
+ @RequiredArgsConstructor
+ public static class WsCmdListHandler {
+ private final java.util.function.Function> cmdsExtractor;
+ private final BiConsumer handler;
+
+ public List extractCmds(W cmdsWrapper) {
+ return cmdsExtractor.apply(cmdsWrapper);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void handle(WebSocketSessionRef sessionRef, List> cmds) {
+ cmds.forEach(cmd -> {
+ handler.accept(sessionRef, (C) cmd);
+ });
+ }
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java b/application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.java
similarity index 96%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java
rename to application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.java
index ffc5009304..591a688809 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/SessionEvent.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/SessionEvent.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;
+package org.thingsboard.server.service.ws;
import lombok.Getter;
import lombok.ToString;
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
similarity index 63%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java
rename to application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
index 234ac1e6bc..dd54b01580 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketMsgEndpoint.java
@@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.telemetry;
+package org.thingsboard.server.service.ws;
import org.springframework.web.socket.CloseStatus;
+import org.thingsboard.server.service.ws.WebSocketSessionRef;
import java.io.IOException;
/**
* Created by ashvayka on 27.03.18.
*/
-public interface TelemetryWebSocketMsgEndpoint {
+public interface WebSocketMsgEndpoint {
- void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException;
+ void send(WebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException;
- void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException;
+ void sendPing(WebSocketSessionRef sessionRef, long currentTime) throws IOException;
- void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException;
+ void close(WebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
similarity index 63%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
rename to application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
index f66a78ae1d..a31c6a0ca0 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketService.java
@@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.telemetry;
+package org.thingsboard.server.service.ws;
import org.springframework.web.socket.CloseStatus;
-import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
-import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate;
-import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
+import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
+import org.thingsboard.server.service.ws.SessionEvent;
+import org.thingsboard.server.service.ws.WebSocketSessionRef;
/**
* Created by ashvayka on 27.03.18.
*/
-public interface TelemetryWebSocketService {
+public interface WebSocketService {
- void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent);
+ void handleWebSocketSessionEvent(WebSocketSessionRef sessionRef, SessionEvent sessionEvent);
- void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg);
+ void handleWebSocketMsg(WebSocketSessionRef sessionRef, String msg);
void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update);
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
similarity index 70%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
rename to application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
index c0fe042a40..e799c8faa0 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionRef.java
@@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.telemetry;
+package org.thingsboard.server.service.ws;
+import lombok.Builder;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.net.InetSocketAddress;
@@ -25,34 +27,25 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by ashvayka on 27.03.18.
*/
-public class TelemetryWebSocketSessionRef {
+@RequiredArgsConstructor
+@Builder
+@Getter
+public class WebSocketSessionRef {
private static final long serialVersionUID = 1L;
- @Getter
private final String sessionId;
- @Getter
private final SecurityUser securityCtx;
- @Getter
private final InetSocketAddress localAddress;
- @Getter
private final InetSocketAddress remoteAddress;
- @Getter
- private final AtomicInteger sessionSubIdSeq;
-
- public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
- this.sessionId = sessionId;
- this.securityCtx = securityCtx;
- this.localAddress = localAddress;
- this.remoteAddress = remoteAddress;
- this.sessionSubIdSeq = new AtomicInteger();
- }
+ private final WebSocketSessionType sessionType;
+ private final AtomicInteger sessionSubIdSeq = new AtomicInteger();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o;
+ WebSocketSessionRef that = (WebSocketSessionRef) o;
return Objects.equals(sessionId, that.sessionId);
}
@@ -63,10 +56,11 @@ public class TelemetryWebSocketSessionRef {
@Override
public String toString() {
- return "TelemetryWebSocketSessionRef{" +
+ return "WebSocketSessionRef{" +
"sessionId='" + sessionId + '\'' +
", localAddress=" + localAddress +
", remoteAddress=" + remoteAddress +
+ ", sessionType=" + sessionType +
'}';
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java
new file mode 100644
index 0000000000..5c1a155c4b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WebSocketSessionType.java
@@ -0,0 +1,38 @@
+/**
+ * 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.ws;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Getter
+public enum WebSocketSessionType {
+ TELEMETRY("telemetry"),
+ NOTIFICATIONS("notifications");
+
+ private final String name;
+
+ public static Optional forName(String name) {
+ return Arrays.stream(values())
+ .filter(sessionType -> sessionType.getName().equals(name))
+ .findFirst();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java b/application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java
similarity index 80%
rename from application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
rename to application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java
index 1c22e65e03..48c2e65509 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
+++ b/application/src/main/java/org/thingsboard/server/service/ws/WsSessionMetaData.java
@@ -13,27 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.telemetry;
+package org.thingsboard.server.service.ws;
/**
* Created by ashvayka on 27.03.18.
*/
public class WsSessionMetaData {
- private TelemetryWebSocketSessionRef sessionRef;
+ private WebSocketSessionRef sessionRef;
private long lastActivityTime;
- public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) {
+ public WsSessionMetaData(WebSocketSessionRef sessionRef) {
super();
this.sessionRef = sessionRef;
this.lastActivityTime = System.currentTimeMillis();
}
- public TelemetryWebSocketSessionRef getSessionRef() {
+ public WebSocketSessionRef getSessionRef() {
return sessionRef;
}
- public void setSessionRef(TelemetryWebSocketSessionRef sessionRef) {
+ public void setSessionRef(WebSocketSessionRef sessionRef) {
this.sessionRef = sessionRef;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
new file mode 100644
index 0000000000..de027c3a30
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
@@ -0,0 +1,240 @@
+/**
+ * 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.ws.notification;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.NotificationCenter;
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.id.NotificationId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.notification.Notification;
+import org.thingsboard.server.common.data.notification.NotificationStatus;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.dao.notification.NotificationService;
+import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
+import org.thingsboard.server.service.ws.WebSocketService;
+import org.thingsboard.server.service.ws.WebSocketSessionRef;
+import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
+import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
+import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
+import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
+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.cmd.v2.CmdUpdate;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
+
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultNotificationCommandsHandler implements NotificationCommandsHandler {
+
+ private final NotificationService notificationService;
+ private final TbLocalSubscriptionService localSubscriptionService;
+ private final NotificationCenter notificationCenter;
+ private final TbServiceInfoProvider serviceInfoProvider;
+ @Autowired @Lazy
+ private WebSocketService wsService;
+
+ @Override
+ public void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd) {
+ log.debug("[{}] Handling unread notifications subscription cmd (cmdId: {})", sessionRef.getSessionId(), cmd.getCmdId());
+ SecurityUser securityCtx = sessionRef.getSecurityCtx();
+ NotificationsSubscription subscription = NotificationsSubscription.builder()
+ .serviceId(serviceInfoProvider.getServiceId())
+ .sessionId(sessionRef.getSessionId())
+ .subscriptionId(cmd.getCmdId())
+ .tenantId(securityCtx.getTenantId())
+ .entityId(securityCtx.getId())
+ .updateProcessor(this::handleNotificationsSubscriptionUpdate)
+ .limit(cmd.getLimit())
+ .build();
+ localSubscriptionService.addSubscription(subscription);
+
+ fetchUnreadNotifications(subscription);
+ sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate());
+ }
+
+ @Override
+ public void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd) {
+ log.debug("[{}] Handling unread notifications count subscription cmd (cmdId: {})", sessionRef.getSessionId(), cmd.getCmdId());
+ SecurityUser securityCtx = sessionRef.getSecurityCtx();
+ NotificationsCountSubscription subscription = NotificationsCountSubscription.builder()
+ .serviceId(serviceInfoProvider.getServiceId())
+ .sessionId(sessionRef.getSessionId())
+ .subscriptionId(cmd.getCmdId())
+ .tenantId(securityCtx.getTenantId())
+ .entityId(securityCtx.getId())
+ .updateProcessor(this::handleNotificationsCountSubscriptionUpdate)
+ .build();
+ localSubscriptionService.addSubscription(subscription);
+
+ fetchUnreadNotificationsCount(subscription);
+ sendUpdate(sessionRef.getSessionId(), subscription.createUpdate());
+ }
+
+ private void fetchUnreadNotifications(NotificationsSubscription subscription) {
+ log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
+ PageData notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(),
+ (UserId) subscription.getEntityId(), subscription.getLimit());
+ subscription.getLatestUnreadNotifications().clear();
+ notifications.getData().forEach(notification -> {
+ subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
+ });
+ subscription.getTotalUnreadCounter().set((int) notifications.getTotalElements());
+ }
+
+ private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
+ log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId());
+ int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId());
+ subscription.getUnreadCounter().set(unreadCount);
+ }
+
+
+ /* Notifications subscription update handling */
+ private void handleNotificationsSubscriptionUpdate(NotificationsSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) {
+ if (subscriptionUpdate.getNotificationUpdate() != null) {
+ handleNotificationUpdate(subscription, subscriptionUpdate.getNotificationUpdate());
+ } else if (subscriptionUpdate.getNotificationRequestUpdate() != null) {
+ handleNotificationRequestUpdate(subscription, subscriptionUpdate.getNotificationRequestUpdate());
+ }
+ }
+
+ private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) {
+ log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
+ Notification notification = update.getNotification();
+ UUID notificationId = update.getNotificationId();
+ if (update.isCreated()) {
+ subscription.getLatestUnreadNotifications().put(notificationId, notification);
+ subscription.getTotalUnreadCounter().incrementAndGet();
+ if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) {
+ Set beyondLimit = subscription.getSortedNotifications().stream().skip(subscription.getLimit())
+ .map(IdBased::getUuidId).collect(Collectors.toSet());
+ beyondLimit.forEach(id -> subscription.getLatestUnreadNotifications().remove(id));
+ }
+ sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
+ } else if (update.isUpdated()) {
+ if (update.getNewStatus() == NotificationStatus.READ) {
+ if (update.isAllNotifications() || subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
+ fetchUnreadNotifications(subscription);
+ sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
+ } else {
+ subscription.getTotalUnreadCounter().decrementAndGet();
+ sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
+ }
+ } else if (notification.getStatus() != NotificationStatus.READ) {
+ if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
+ subscription.getLatestUnreadNotifications().put(notificationId, notification);
+ sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
+ }
+ }
+ } else if (update.isDeleted()) {
+ if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
+ fetchUnreadNotifications(subscription);
+ sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
+ } else if (notification.getStatus() != NotificationStatus.READ) {
+ subscription.getTotalUnreadCounter().decrementAndGet();
+ sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
+ }
+ }
+ }
+
+ private void handleNotificationRequestUpdate(NotificationsSubscription subscription, NotificationRequestUpdate update) {
+ log.trace("[{}, subId: {}] Handling notification request update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
+ fetchUnreadNotifications(subscription);
+ sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
+ }
+
+
+ /* Notifications count subscription update handling */
+ private void handleNotificationsCountSubscriptionUpdate(NotificationsCountSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) {
+ if (subscriptionUpdate.getNotificationUpdate() != null) {
+ handleNotificationUpdate(subscription, subscriptionUpdate.getNotificationUpdate());
+ } else if (subscriptionUpdate.getNotificationRequestUpdate() != null) {
+ handleNotificationRequestUpdate(subscription, subscriptionUpdate.getNotificationRequestUpdate());
+ }
+ }
+
+ private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) {
+ log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
+ if (update.isCreated()) {
+ subscription.getUnreadCounter().incrementAndGet();
+ sendUpdate(subscription.getSessionId(), subscription.createUpdate());
+ } else if (update.isUpdated()) {
+ if (update.getNewStatus() == NotificationStatus.READ) {
+ if (update.isAllNotifications()) {
+ fetchUnreadNotificationsCount(subscription);
+ } else {
+ subscription.getUnreadCounter().decrementAndGet();
+ }
+ sendUpdate(subscription.getSessionId(), subscription.createUpdate());
+ }
+ } else if (update.isDeleted()) {
+ if (update.getNotification().getStatus() != NotificationStatus.READ) {
+ subscription.getUnreadCounter().decrementAndGet();
+ sendUpdate(subscription.getSessionId(), subscription.createUpdate());
+ }
+ }
+ }
+
+ private void handleNotificationRequestUpdate(NotificationsCountSubscription subscription, NotificationRequestUpdate update) {
+ log.trace("[{}, subId: {}] Handling notification request update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
+ fetchUnreadNotificationsCount(subscription);
+ sendUpdate(subscription.getSessionId(), subscription.createUpdate());
+ }
+
+
+ @Override
+ public void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationsAsReadCmd cmd) {
+ SecurityUser securityCtx = sessionRef.getSecurityCtx();
+ cmd.getNotifications().stream()
+ .map(NotificationId::new)
+ .forEach(notificationId -> {
+ notificationCenter.markNotificationAsRead(securityCtx.getTenantId(), securityCtx.getId(), notificationId);
+ });
+ }
+
+ @Override
+ public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) {
+ SecurityUser securityCtx = sessionRef.getSecurityCtx();
+ notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId());
+ }
+
+ @Override
+ public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
+ localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId());
+ }
+
+ private void sendUpdate(String sessionId, CmdUpdate update) {
+ log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update);
+ wsService.sendWsMsg(sessionId, update);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java
new file mode 100644
index 0000000000..2b27166106
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java
@@ -0,0 +1,37 @@
+/**
+ * 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.ws.notification;
+
+import org.thingsboard.server.service.ws.WebSocketSessionRef;
+import org.thingsboard.server.service.ws.notification.cmd.MarkAllNotificationsAsReadCmd;
+import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationsAsReadCmd;
+import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
+import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
+
+public interface NotificationCommandsHandler {
+
+ void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd);
+
+ void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd);
+
+ void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationsAsReadCmd cmd);
+
+ void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd);
+
+ void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.java
new file mode 100644
index 0000000000..f096106135
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkAllNotificationsAsReadCmd.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.ws.notification.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MarkAllNotificationsAsReadCmd implements WsCmd {
+ private int cmdId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java
new file mode 100644
index 0000000000..55de75387f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationsAsReadCmd.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MarkNotificationsAsReadCmd implements WsCmd {
+ private int cmdId;
+ private List notifications;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java
new file mode 100644
index 0000000000..88613eb966
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.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.ws.notification.cmd;
+
+import lombok.Data;
+
+@Data
+public class NotificationCmdsWrapper {
+
+ private NotificationsCountSubCmd unreadCountSubCmd;
+
+ private NotificationsSubCmd unreadSubCmd;
+
+ private MarkNotificationsAsReadCmd markAsReadCmd;
+
+ private MarkAllNotificationsAsReadCmd markAllAsReadCmd;
+
+ private NotificationsUnsubCmd unsubCmd;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java
new file mode 100644
index 0000000000..0d6757a6e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.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.ws.notification.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotificationsCountSubCmd implements WsCmd {
+ private int cmdId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java
new file mode 100644
index 0000000000..e022d2c49f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotificationsSubCmd implements WsCmd {
+ private int cmdId;
+ private int limit;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java
new file mode 100644
index 0000000000..c9f0897eeb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd {
+ private int cmdId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java
new file mode 100644
index 0000000000..93f51a965b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java
@@ -0,0 +1,46 @@
+/**
+ * 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.ws.notification.cmd;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
+
+@Getter
+@ToString
+public class UnreadNotificationsCountUpdate extends CmdUpdate {
+
+ private final int totalUnreadCount;
+
+ @Builder
+ @JsonCreator
+ public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
+ @JsonProperty("errorMsg") String errorMsg,
+ @JsonProperty("totalUnreadCount") int totalUnreadCount) {
+ super(cmdId, errorCode, errorMsg);
+ this.totalUnreadCount = totalUnreadCount;
+ }
+
+ @Override
+ public CmdUpdateType getCmdUpdateType() {
+ return CmdUpdateType.NOTIFICATIONS_COUNT;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.java
new file mode 100644
index 0000000000..e64624d16e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsUpdate.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.ws.notification.cmd;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.notification.Notification;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
+
+import java.util.Collection;
+
+@Getter
+@ToString(exclude = "notifications")
+public class UnreadNotificationsUpdate extends CmdUpdate {
+
+ private final Collection notifications;
+ private final Notification update;
+ private final int totalUnreadCount;
+
+ @Builder
+ @JsonCreator
+ public UnreadNotificationsUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
+ @JsonProperty("errorMsg") String errorMsg,
+ @JsonProperty("notifications") Collection notifications,
+ @JsonProperty("update") Notification update,
+ @JsonProperty("totalUnreadCount") int totalUnreadCount) {
+ super(cmdId, errorCode, errorMsg);
+ this.notifications = notifications;
+ this.update = update;
+ this.totalUnreadCount = totalUnreadCount;
+ }
+
+ @Override
+ public CmdUpdateType getCmdUpdateType() {
+ return CmdUpdateType.NOTIFICATIONS;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java
new file mode 100644
index 0000000000..97bfeab70c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/WsCmd.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.cmd;
+
+public interface WsCmd {
+ int getCmdId();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java
new file mode 100644
index 0000000000..ebbcc716ab
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.id.NotificationRequestId;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class NotificationRequestUpdate {
+ private NotificationRequestId notificationRequestId;
+ private boolean deleted;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java
new file mode 100644
index 0000000000..d9d8ea5b85
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.sub;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.id.NotificationId;
+import org.thingsboard.server.common.data.notification.Notification;
+import org.thingsboard.server.common.data.notification.NotificationStatus;
+
+import java.util.UUID;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class NotificationUpdate {
+
+ private NotificationId notificationId;
+
+ private boolean created;
+ private Notification notification;
+
+ private boolean updated;
+ private NotificationStatus newStatus;
+ private boolean allNotifications;
+
+ private boolean deleted;
+
+ @JsonIgnore
+ public UUID getNotificationId() {
+ return notificationId != null ? notificationId.getId() :
+ notification != null ? notification.getUuidId() : null;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
new file mode 100644
index 0000000000..cb5d140d21
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.ws.notification.sub;
+
+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.subscription.TbSubscription;
+import org.thingsboard.server.service.subscription.TbSubscriptionType;
+import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+
+@Getter
+public class NotificationsCountSubscription extends TbSubscription {
+
+ private final AtomicInteger unreadCounter = new AtomicInteger();
+
+ @Builder
+ public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
+ BiConsumer updateProcessor) {
+ super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor);
+ }
+
+ public UnreadNotificationsCountUpdate createUpdate() {
+ return UnreadNotificationsCountUpdate.builder()
+ .cmdId(getSubscriptionId())
+ .totalUnreadCount(unreadCounter.get())
+ .build();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
new file mode 100644
index 0000000000..591781a5f6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
@@ -0,0 +1,81 @@
+/**
+ * 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.ws.notification.sub;
+
+import lombok.Builder;
+import lombok.Getter;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.Notification;
+import org.thingsboard.server.service.subscription.TbSubscription;
+import org.thingsboard.server.service.subscription.TbSubscriptionType;
+import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+@Getter
+public class NotificationsSubscription extends TbSubscription {
+
+ private final Map