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 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, 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 latestUnreadNotifications = new HashMap<>(); + private final int limit; + private final AtomicInteger totalUnreadCounter = new AtomicInteger(); + + @Builder + public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, + BiConsumer updateProcessor, + int limit) { + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS, updateProcessor); + this.limit = limit; + } + + public UnreadNotificationsUpdate createFullUpdate() { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .notifications(getSortedNotifications()) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + + public List getSortedNotifications() { + return latestUnreadNotifications.values().stream() + .sorted(Comparator.comparing(BaseData::getCreatedTime, Comparator.reverseOrder())) + .collect(Collectors.toList()); + } + + public UnreadNotificationsUpdate createPartialUpdate(Notification notification) { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .update(notification) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + + public UnreadNotificationsUpdate createCountUpdate() { + return UnreadNotificationsUpdate.builder() + .cmdId(getSubscriptionId()) + .totalUnreadCount(totalUnreadCounter.get()) + .build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java new file mode 100644 index 0000000000..b69039cbfe --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.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.ws.notification.sub; + +import lombok.Data; + +@Data +public class NotificationsSubscriptionUpdate { + + private final NotificationUpdate notificationUpdate; + private final NotificationRequestUpdate notificationRequestUpdate; + + public NotificationsSubscriptionUpdate(NotificationUpdate notificationUpdate) { + this.notificationUpdate = notificationUpdate; + this.notificationRequestUpdate = null; + } + + public NotificationsSubscriptionUpdate(NotificationRequestUpdate notificationRequestUpdate) { + this.notificationUpdate = null; + this.notificationRequestUpdate = notificationRequestUpdate; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.java index 29a0023dd3..c5324208ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryFeature.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryFeature.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.telemetry; /** * Created by ashvayka on 08.05.17. diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java index 201c0acb5c..6de98ac8a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/TelemetryWebSocketTextMsg.java @@ -13,9 +13,10 @@ * 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.telemetry; import lombok.Data; +import org.thingsboard.server.service.ws.WebSocketSessionRef; /** * Created by ashvayka on 27.03.18. @@ -23,7 +24,7 @@ import lombok.Data; @Data public class TelemetryWebSocketTextMsg { - private final TelemetryWebSocketSessionRef sessionRef; + private final WebSocketSessionRef sessionRef; private final String payload; } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java similarity index 62% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java index a2ccf01c9b..bea4772085 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd; +package org.thingsboard.server.service.ws.telemetry.cmd; import lombok.Data; -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.TimeseriesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; +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.TimeseriesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUnsubscribeCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +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.EntityDataUnsubscribeCmd; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java index f054c9f530..1ccec261b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/AttributesSubscriptionCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/AttributesSubscriptionCmd.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v1; +package org.thingsboard.server.service.ws.telemetry.cmd.v1; import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; /** * @author Andrew Shvayka diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.java index 442acb5f5c..6791066641 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/GetHistoryCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/GetHistoryCmd.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.cmd.v1; +package org.thingsboard.server.service.ws.telemetry.cmd.v1; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java index 7e6c40009b..6a5f9d820d 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/SubscriptionCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/SubscriptionCmd.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v1; +package org.thingsboard.server.service.ws.telemetry.cmd.v1; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; @NoArgsConstructor @AllArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.java index e8061a513d..dc9d3a48c2 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TelemetryPluginCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TelemetryPluginCmd.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.cmd.v1; +package org.thingsboard.server.service.ws.telemetry.cmd.v1; /** * @author Andrew Shvayka diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java index 903ec92373..f0a7b9d3af 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v1/TimeseriesSubscriptionCmd.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v1; +package org.thingsboard.server.service.ws.telemetry.cmd.v1; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.thingsboard.server.service.telemetry.TelemetryFeature; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; /** * @author Andrew Shvayka diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.java index 34eb00de52..b928363fc4 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggHistoryCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.java index 9ab4b8064c..dc8966c0d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggKey.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; import org.thingsboard.server.common.data.kv.Aggregation; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.java index d6986b6163..f9dc34fb75 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AggTimeSeriesCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.java index b2e6b3deef..0f83d00d6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java index 822038ee44..97db558cf9 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUnsubscribeCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.java index 1cda950958..7d7d29300a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/AlarmDataUpdate.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -21,7 +21,7 @@ import lombok.Getter; import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.java index 5a72b719b2..ba779176aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdate.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java similarity index 85% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java index c132cf6b30..e1a9b32895 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; public enum CmdUpdateType { ENTITY_DATA, ALARM_DATA, - COUNT_DATA + COUNT_DATA, + NOTIFICATIONS, + NOTIFICATIONS_COUNT } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java similarity index 89% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java index 4f02260e11..c09cbc03d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataCmd.java @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; import lombok.Getter; -import lombok.NoArgsConstructor; @Data public class DataCmd { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java index 9ac80b7987..c4bfce441a 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/DataUpdate.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Getter; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java index ece51ed0a2..c39f81f1b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountCmd.java @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import org.thingsboard.server.common.data.query.EntityCountQuery; -import org.thingsboard.server.common.data.query.EntityDataQuery; public class EntityCountCmd extends DataCmd { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java index b522d1e193..b82fecd8fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUnsubscribeCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java similarity index 85% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java index 4df0f4bd29..030d39b8dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityCountUpdate.java @@ -13,17 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.telemetry.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.ToString; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.EntityData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; - -import java.util.List; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; @ToString public class EntityCountUpdate extends CmdUpdate { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java similarity index 97% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.java index 41fa2f625b..5fe9179651 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java index 294462ef66..80577edda2 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUnsubscribeCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.java index 8081037e05..3b3cf69186 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityDataUpdate.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -21,7 +21,7 @@ import lombok.Getter; import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import java.util.List; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.java index d7132ace8b..5f22897b8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityHistoryCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/EntityHistoryCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; import org.thingsboard.server.common.data.kv.Aggregation; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.java index 4b04f952d0..5d6a730772 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/GetTsCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/GetTsCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import org.thingsboard.server.common.data.kv.Aggregation; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.java index 926b14eb5d..291829b47b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/LatestValueCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/LatestValueCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import lombok.Data; import org.thingsboard.server.common.data.query.EntityKey; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.java index c01b2d113b..6ece880d7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/TimeSeriesCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/TimeSeriesCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.java index 05766f6a07..81288c76da 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/UnsubscribeCmd.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.cmd.v2; +package org.thingsboard.server.service.ws.telemetry.cmd.v2; public interface UnsubscribeCmd { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java similarity index 91% rename from application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java index 4961f901c5..90041ffa13 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/AlarmSubscriptionUpdate.java @@ -13,12 +13,11 @@ * 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.ws.telemetry.sub; import lombok.Getter; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; public class AlarmSubscriptionUpdate { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java index 616095320e..88d8f7ca7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/SubscriptionState.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/SubscriptionState.java @@ -13,13 +13,13 @@ * 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.ws.telemetry.sub; import lombok.AllArgsConstructor; 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.TelemetryFeature; +import org.thingsboard.server.service.ws.telemetry.TelemetryFeature; import java.util.Map; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java rename to application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java index 6a695c59e4..108c5cf1d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/TelemetrySubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -13,9 +13,10 @@ * 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.ws.telemetry.sub; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import java.util.ArrayList; import java.util.Collections; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 10c4c9edac..d96e50ca7f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -281,6 +281,8 @@ sql: partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week alarm_comments: partition_size: "${SQL_ALARM_COMMENTS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week + notifications: + partition_size: "${SQL_NOTIFICATIONS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:true}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert @@ -323,6 +325,10 @@ sql: enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}" ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. Accuracy of the cleanup depends on the sql.audit_logs.partition_size checking_interval_ms: "${SQL_TTL_AUDIT_LOGS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day + notifications: + enabled: "${SQL_TTL_NOTIFICATIONS_ENABLED:true}" + ttl: "${SQL_TTL_NOTIFICATIONS_SECS:2592000}" # Default value - 30 days + checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day relations: max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # This value has to be reasonable small to prevent infinite recursion as early as possible pool_size: "${SQL_RELATIONS_POOL_SIZE:4}" # This value has to be reasonable small to prevent relation query blocking all other DB calls @@ -431,6 +437,12 @@ cache: assetProfiles: timeToLiveInMinutes: "${CACHE_SPECS_ASSET_PROFILES_TTL:1440}" maxSize: "${CACHE_SPECS_ASSET_PROFILES_MAX_SIZE:10000}" + notificationRules: + timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:1440}" + maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:10000}" + notificationSettings: + timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_SETTINGS_TTL:10}" + maxSize: "${CACHE_SPECS_NOTIFICATION_SETTINGS_MAX_SIZE:1000}" attributes: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" @@ -1206,6 +1218,9 @@ vc: io_pool_size: "${TB_VC_GIT_POOL_SIZE:3}" repositories-folder: "${TB_VC_GIT_REPOSITORIES_FOLDER:${java.io.tmpdir}/repositories}" +notification_system: + thread_pool_size: "${TB_NOTIFICATION_SYSTEM_THREAD_POOL_SIZE:10}" + management: endpoints: web: diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index 32d1f65765..5809ab5e42 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -53,6 +53,7 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { protected int wsPort; private volatile TbTestWebSocketClient wsClient; // lazy + private volatile TbTestWebSocketClient anotherWsClient; // lazy public TbTestWebSocketClient getWsClient() { if (wsClient == null) { @@ -69,6 +70,21 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { return wsClient; } + public TbTestWebSocketClient getAnotherWsClient() { + if (anotherWsClient == null) { + synchronized (this) { + try { + if (anotherWsClient == null) { + anotherWsClient = buildAndConnectWebSocketClient(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + return anotherWsClient; + } + @Before public void beforeWsTest() throws Exception { // placeholder @@ -79,9 +95,12 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { if (wsClient != null) { wsClient.close(); } + if (anotherWsClient != null) { + anotherWsClient.close(); + } } - private TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token)); assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); return wsClient; diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index c9b739bad1..b93e74c959 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -69,9 +69,11 @@ import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -98,6 +100,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; @@ -136,6 +140,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC @Slf4j public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { + public static final int TIMEOUT = 30; protected ObjectMapper mapper = new ObjectMapper(); @@ -352,26 +357,30 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected Tenant savedDifferentTenant; protected User savedDifferentTenantUser; private Customer savedDifferentCustomer; + protected User differentCustomerUser; protected void loginDifferentTenant() throws Exception { if (savedDifferentTenant != null) { login(DIFFERENT_TENANT_ADMIN_EMAIL, DIFFERENT_TENANT_ADMIN_PASSWORD); } else { - loginSysAdmin(); - - Tenant tenant = new Tenant(); - tenant.setTitle(TEST_DIFFERENT_TENANT_NAME); - savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); - differentTenantId = savedDifferentTenant.getId(); - Assert.assertNotNull(savedDifferentTenant); - User differentTenantAdmin = new User(); - differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); - differentTenantAdmin.setEmail(DIFFERENT_TENANT_ADMIN_EMAIL); - savedDifferentTenantUser = createUserAndLogin(differentTenantAdmin, DIFFERENT_TENANT_ADMIN_PASSWORD); + createDifferentTenant(); } } + protected void createDifferentTenant() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle(TEST_DIFFERENT_TENANT_NAME); + savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); + differentTenantId = savedDifferentTenant.getId(); + Assert.assertNotNull(savedDifferentTenant); + User differentTenantAdmin = new User(); + differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); + differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); + differentTenantAdmin.setEmail(DIFFERENT_TENANT_ADMIN_EMAIL); + savedDifferentTenantUser = createUserAndLogin(differentTenantAdmin, DIFFERENT_TENANT_ADMIN_PASSWORD); + } + protected void loginDifferentCustomer() throws Exception { if (savedDifferentCustomer != null) { login(savedDifferentCustomer.getEmail(), CUSTOMER_USER_PASSWORD); @@ -379,7 +388,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { createDifferentCustomer(); loginTenantAdmin(); - User differentCustomerUser = new User(); + differentCustomerUser = new User(); differentCustomerUser.setAuthority(Authority.CUSTOMER_USER); differentCustomerUser.setTenantId(tenantId); differentCustomerUser.setCustomerId(savedDifferentCustomer.getId()); @@ -542,6 +551,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return protoTransportPayloadConfiguration; } + protected Device createDevice(String deviceName, String type, String accessToken) throws Exception { + Device device = new Device(); + device.setName(deviceName); + device.setType(type); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + credentials.setCredentialsId(accessToken); + + SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(device, credentials); + return doPost("/api/device-with-credentials", request, Device.class); + } protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); @@ -643,7 +664,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); } - protected T doPost(String urlTemplate, T content, Class responseClass, String... params) { + protected R doPost(String urlTemplate, T content, Class responseClass, String... params) { try { return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); } catch (Exception e) { @@ -769,10 +790,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } public class IdComparator implements Comparator { + @Override public int compare(D o1, D o2) { return o1.getId().getId().compareTo(o2.getId().getId()); } + } protected static ResultMatcher statusReason(Matcher matcher) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 6a94cc5a1e..7cf6d7109a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -656,4 +656,38 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { return foundAlarm; } + + + @Test + public void testCreateAlarmWithOtherTenantsAssignee() throws Exception { + loginDifferentTenant(); + loginTenantAdmin(); + + Alarm alarm = Alarm.builder() + .tenantId(tenantId) + .customerId(customerId) + .originator(customerDevice.getId()) + .severity(AlarmSeverity.CRITICAL) + .assigneeId(savedDifferentTenantUser.getId()) + .build(); + + doPost("/api/alarm", alarm).andExpect(status().isForbidden()); + } + + @Test + public void testCreateAlarmWithOtherCustomerAsAssignee() throws Exception { + loginDifferentCustomer(); + loginCustomerUser(); + + Alarm alarm = Alarm.builder() + .tenantId(tenantId) + .customerId(customerId) + .originator(customerDevice.getId()) + .severity(AlarmSeverity.CRITICAL) + .assigneeId(differentCustomerUser.getId()) + .build(); + + doPost("/api/alarm", alarm).andExpect(status().isForbidden()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index 6bb747cbfe..9b05b38a22 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -671,7 +671,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { doPost("/api/device/credentials", deviceCredentials) .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Incorrect deviceId null"))); + .andExpect(statusReason(containsString("Invalid entity id"))); testNotifyEntityNever(deviceCredentials.getDeviceId(), new Device()); testNotificationUpdateGatewayNever(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index aeebc7bfee..f29827fbe2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -92,7 +92,7 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe } @Test - public void testCountEntitiesByQuery() throws Exception { + public void testTenantCountEntitiesByQuery() throws Exception { List devices = new ArrayList<>(); for (int i = 0; i < 97; i++) { Device device = new Device(); @@ -139,6 +139,56 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Assert.assertEquals(97, count2.longValue()); } + @Test + public void testSysAdminCountEntitiesByQuery() throws Exception { + List devices = new ArrayList<>(); + for (int i = 0; i < 97; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + devices.add(doPost("/api/device", device, Device.class)); + Thread.sleep(1); + } + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceType("default"); + filter.setDeviceNameFilter(""); + + loginSysAdmin(); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + Long count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + filter.setDeviceType("unknown"); + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(0, count.longValue()); + + filter.setDeviceType("default"); + filter.setDeviceNameFilter("Device1"); + + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(11, count.longValue()); + + EntityListFilter entityListFilter = new EntityListFilter(); + entityListFilter.setEntityType(EntityType.DEVICE); + entityListFilter.setEntityList(devices.stream().map(Device::getId).map(DeviceId::toString).collect(Collectors.toList())); + + countQuery = new EntityCountQuery(entityListFilter); + + count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); + Assert.assertEquals(97, count.longValue()); + + EntityTypeFilter filter2 = new EntityTypeFilter(); + filter2.setEntityType(EntityType.DEVICE); + + EntityCountQuery countQuery2 = new EntityCountQuery(filter2); + + Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class); + Assert.assertEquals(97, count2.longValue()); + } + @Test public void testSimpleFindEntityDataByQuery() throws Exception { List devices = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java new file mode 100644 index 0000000000..b3032399c2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java @@ -0,0 +1,372 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.FeaturesInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.ApiUsageStateFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityTypeFilter; +import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +public abstract class BaseHomePageApiTest extends AbstractControllerTest { + + @Autowired + private TbApiUsageStateClient apiUsageStateClient; + + //For system administrator + @Test + public void testTenantsCountWsCmd() throws Exception { + loginSysAdmin(); + + List tenants = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Tenant tenant = new Tenant(); + tenant.setTitle("tenant" + i); + tenants.add(doPost("/api/tenant", tenant, Tenant.class)); + } + + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.TENANT); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(101, update.getCount()); + + for (Tenant tenant : tenants) { + doDelete("/api/tenant/" + tenant.getId().toString()); + } + } + + @Test + public void testTenantProfilesCountWsCmd() throws Exception { + loginSysAdmin(); + + List tenantProfiles = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("tenantProfile" + i); + tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class)); + } + + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.TENANT_PROFILE); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(101, update.getCount()); + + for (TenantProfile tenantProfile : tenantProfiles) { + doDelete("/api/tenantProfile/" + tenantProfile.getId().toString()); + } + } + + @Test + public void testUsersCountWsCmd() throws Exception { + loginSysAdmin(); + + List users = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + User user = new User(); + user.setEmail(i + "user@thingsboard.org"); + user.setTenantId(tenantId); + user.setAuthority(Authority.TENANT_ADMIN); + users.add(doPost("/api/user", user, User.class)); + } + + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.USER); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(103, update.getCount()); + + for (User user : users) { + doDelete("/api/user/" + user.getId().toString()); + } + } + + @Test + public void testCustomersCountWsCmd() throws Exception { + loginTenantAdmin(); + + List customers = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Customer customer = new Customer(); + customer.setTitle("customer" + i); + customers.add(doPost("/api/customer", customer, Customer.class)); + } + + loginSysAdmin(); + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.CUSTOMER); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(101, update.getCount()); + + loginTenantAdmin(); + for (Customer customer : customers) { + doDelete("/api/customer/" + customer.getId().toString()); + } + } + + @Test + public void testDevicesCountWsCmd() throws Exception { + loginTenantAdmin(); + + List devices = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Device device = new Device(); + device.setName("device" + i); + devices.add(doPost("/api/device", device, Device.class)); + } + + loginSysAdmin(); + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.DEVICE); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(100, update.getCount()); + + loginTenantAdmin(); + for (Device device : devices) { + doDelete("/api/device/" + device.getId().toString()); + } + } + + @Test + public void testAssetsCountWsCmd() throws Exception { + loginTenantAdmin(); + + List assets = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + Asset asset = new Asset(); + asset.setName("asset" + i); + assets.add(doPost("/api/asset", asset, Asset.class)); + } + + loginSysAdmin(); + EntityTypeFilter ef = new EntityTypeFilter(); + ef.setEntityType(EntityType.ASSET); + EntityCountCmd cmd = new EntityCountCmd(1, new EntityCountQuery(ef, Collections.emptyList())); + getWsClient().send(cmd); + EntityCountUpdate update = getWsClient().parseCountReply(getWsClient().waitForReply()); + Assert.assertEquals(1, update.getCmdId()); + Assert.assertEquals(100, update.getCount()); + + loginTenantAdmin(); + for (Asset asset : assets) { + doDelete("/api/asset/" + asset.getId().toString()); + } + } + + @Test + public void testSystemInfoTimeSeriesWsCmd() throws Exception { + ApiUsageState apiUsageState = apiUsageStateClient.getApiUsageState(TenantId.SYS_TENANT_ID); + Assert.assertNotNull(apiUsageState); + + loginSysAdmin(); + long now = System.currentTimeMillis(); + + EntityDataUpdate update = getWsClient().sendEntityDataQuery(new ApiUsageStateFilter()); + + Assert.assertEquals(1, update.getCmdId()); + PageData pageData = update.getData(); + Assert.assertNotNull(pageData); + Assert.assertEquals(1, pageData.getData().size()); + Assert.assertEquals(apiUsageState.getId(), pageData.getData().get(0).getEntityId()); + + update = getWsClient().subscribeTsUpdate( + List.of("memoryUsage", "totalMemory", "freeMemory", "cpuUsage", "totalCpuUsage", "freeDiscSpace", "totalDiscSpace"), + now, TimeUnit.HOURS.toMillis(1)); + Assert.assertEquals(1, update.getCmdId()); + List listData = update.getUpdate(); + Assert.assertNotNull(listData); + Assert.assertEquals(1, listData.size()); + Assert.assertEquals(apiUsageState.getId(), listData.get(0).getEntityId()); + Assert.assertEquals(7, listData.get(0).getTimeseries().size()); + + for (TsValue[] tsv : listData.get(0).getTimeseries().values()) { + Assert.assertTrue(tsv.length > 1); + } + } + + @Test + public void testGetFeaturesInfo() throws Exception { + loginSysAdmin(); + + FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertNotNull(featuresInfo); + Assert.assertFalse(featuresInfo.isEmailEnabled()); + Assert.assertFalse(featuresInfo.isSmsEnabled()); + Assert.assertFalse(featuresInfo.isTwoFaEnabled()); + Assert.assertFalse(featuresInfo.isNotificationEnabled()); + Assert.assertFalse(featuresInfo.isOauthEnabled()); + + AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class); + + JsonNode jsonValue = mailSettings.getJsonValue(); + ((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org"); + mailSettings.setJsonValue(jsonValue); + + doPost("/api/admin/settings", mailSettings).andExpect(status().isOk()); + + featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertTrue(featuresInfo.isEmailEnabled()); + Assert.assertFalse(featuresInfo.isSmsEnabled()); + Assert.assertFalse(featuresInfo.isTwoFaEnabled()); + Assert.assertFalse(featuresInfo.isNotificationEnabled()); + Assert.assertFalse(featuresInfo.isOauthEnabled()); + + AdminSettings smsSettings = new AdminSettings(); + smsSettings.setKey("sms"); + smsSettings.setJsonValue(JacksonUtil.newObjectNode()); + doPost("/api/admin/settings", smsSettings).andExpect(status().isOk()); + + featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertTrue(featuresInfo.isEmailEnabled()); + Assert.assertTrue(featuresInfo.isSmsEnabled()); + Assert.assertFalse(featuresInfo.isTwoFaEnabled()); + Assert.assertFalse(featuresInfo.isNotificationEnabled()); + Assert.assertFalse(featuresInfo.isOauthEnabled()); + + AdminSettings twoFaSettingsSettings = new AdminSettings(); + twoFaSettingsSettings.setKey("twoFaSettings"); + twoFaSettingsSettings.setJsonValue(JacksonUtil.newObjectNode()); + doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk()); + + featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertTrue(featuresInfo.isEmailEnabled()); + Assert.assertTrue(featuresInfo.isSmsEnabled()); + Assert.assertTrue(featuresInfo.isTwoFaEnabled()); + Assert.assertFalse(featuresInfo.isNotificationEnabled()); + Assert.assertFalse(featuresInfo.isOauthEnabled()); + + AdminSettings notificationsSettings = new AdminSettings(); + notificationsSettings.setKey("notifications"); + notificationsSettings.setJsonValue(JacksonUtil.newObjectNode()); + doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk()); + + featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertTrue(featuresInfo.isEmailEnabled()); + Assert.assertTrue(featuresInfo.isSmsEnabled()); + Assert.assertTrue(featuresInfo.isTwoFaEnabled()); + Assert.assertTrue(featuresInfo.isNotificationEnabled()); + Assert.assertFalse(featuresInfo.isOauthEnabled()); + + OAuth2Info oAuth2Info = createDefaultOAuth2Info(); + + doPost("/api/oauth2/config", oAuth2Info).andExpect(status().isOk()); + + featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); + Assert.assertNotNull(featuresInfo); + Assert.assertTrue(featuresInfo.isEmailEnabled()); + Assert.assertTrue(featuresInfo.isSmsEnabled()); + Assert.assertTrue(featuresInfo.isTwoFaEnabled()); + Assert.assertTrue(featuresInfo.isNotificationEnabled()); + Assert.assertTrue(featuresInfo.isOauthEnabled()); + } + + + private OAuth2Info createDefaultOAuth2Info() { + return new OAuth2Info(true, Lists.newArrayList( + OAuth2ParamsInfo.builder() + .domainInfos(Lists.newArrayList( + OAuth2DomainInfo.builder().name("domain").scheme(SchemeType.MIXED).build() + )) + .mobileInfos(Collections.emptyList()) + .clientRegistrations(Lists.newArrayList( + validRegistrationInfo() + )) + .build() + )); + } + + private OAuth2RegistrationInfo validRegistrationInfo() { + return OAuth2RegistrationInfo.builder() + .clientId(UUID.randomUUID().toString()) + .clientSecret(UUID.randomUUID().toString()) + .authorizationUri(UUID.randomUUID().toString()) + .accessTokenUri(UUID.randomUUID().toString()) + .scope(Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .platforms(Collections.emptyList()) + .userInfoUri(UUID.randomUUID().toString()) + .userNameAttributeName(UUID.randomUUID().toString()) + .jwkSetUri(UUID.randomUUID().toString()) + .clientAuthenticationMethod(UUID.randomUUID().toString()) + .loginButtonLabel(UUID.randomUUID().toString()) + .mapperConfig( + OAuth2MapperConfig.builder() + .type(MapperType.CUSTOM) + .custom( + OAuth2CustomMapperConfig.builder() + .url(UUID.randomUUID().toString()) + .build() + ) + .build() + ) + .build(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java index 18dc371a80..2e42009e5b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java @@ -511,7 +511,7 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { for (Queue queue : foundTenantQueues) { doGet("/api/queues/" + queue.getId()) .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString(msgErrorNotFound))); + .andExpect(statusReason(containsString(msgErrorNoFound("Queue", queue.getId().toString())))); } loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index ce354749f3..d26f6b190d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -48,12 +48,12 @@ import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.service.subscription.SubscriptionErrorCode; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; -import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import java.util.Arrays; import java.util.Collections; diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 1f9ac4ea69..2366a631fa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import lombok.Getter; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; @@ -27,15 +28,15 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; -import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; -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.LatestValueCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; +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.v2.EntityCountCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; +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.LatestValueCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; import java.net.URI; import java.nio.channels.NotYetConnectedException; @@ -48,6 +49,8 @@ import java.util.concurrent.TimeUnit; public class TbTestWebSocketClient extends WebSocketClient { private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30); + + @Getter private volatile String lastMsg; private volatile CountDownLatch reply; private volatile CountDownLatch update; @@ -113,35 +116,59 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TIMEOUT); + return waitForUpdate(false); + } + + public String waitForUpdate(boolean throwExceptionOnTimeout) { + return waitForUpdate(TIMEOUT, throwExceptionOnTimeout); } public String waitForUpdate(long ms) { + return waitForUpdate(ms, false); + } + + public String waitForUpdate(long ms, boolean throwExceptionOnTimeout) { log.debug("waitForUpdate [{}]", ms); try { - if (!update.await(ms, TimeUnit.MILLISECONDS)) { + if (update.await(ms, TimeUnit.MILLISECONDS)) { + return lastMsg; + } else { log.warn("Failed to await update (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace")); } } catch (InterruptedException e) { log.warn("Failed to await update", e); } - return lastMsg; + if (throwExceptionOnTimeout) { + throw new AssertionError("Waited for update for " + ms + " ms but none arrived"); + } else { + return null; + } } public String waitForReply() { - return waitForReply(TIMEOUT); + return waitForReply(false); + } + + public String waitForReply(boolean throwExceptionOnTimeout) { + return waitForReply(TIMEOUT, throwExceptionOnTimeout); } - public String waitForReply(long ms) { + public String waitForReply(long ms, boolean throwExceptionOnTimeout) { log.debug("waitForReply [{}]", ms); try { - if (!reply.await(ms, TimeUnit.MILLISECONDS)) { + if (reply.await(ms, TimeUnit.MILLISECONDS)) { + return lastMsg; + } else { log.warn("Failed to await reply (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace")); } } catch (InterruptedException e) { log.warn("Failed to await reply", e); } - return lastMsg; + if (throwExceptionOnTimeout) { + throw new AssertionError("Waited for reply for " + ms + " ms but none arrived"); + } else { + return null; + } } public EntityDataUpdate parseDataReply(String msg) { diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index ea94d4e767..0f77eb0fd2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -124,9 +124,9 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { .andExpect(status().isBadRequest())); assertThat(errorMessage).contains( - "verification code check rate limit configuration is invalid", - "maximum number of verification failure before user lockout must be positive", - "total amount of time allotted for verification must be greater than or equal 60" + "verificationCodeCheckRateLimit is invalid", + "maxVerificationFailuresBeforeUserLockout must be positive", + "totalAllowedTimeForVerification must be greater than or equal to 60" ); } @@ -136,7 +136,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { invalidTotpTwoFaProviderConfig.setIssuerName(" "); String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig); - assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank"); + assertThat(errorResponse).containsIgnoringCase("issuerName must not be blank"); } @Test @@ -151,8 +151,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null); invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0); errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig); - assertThat(errorResponse).containsIgnoringCase("verification message template is required"); - assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required"); + assertThat(errorResponse).containsIgnoringCase("smsVerificationMessageTemplate is required"); + assertThat(errorResponse).containsIgnoringCase("verificationCodeLifetime is required"); } private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { @@ -216,12 +216,12 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) .andExpect(status().isBadRequest())); - assertThat(errorMessage).containsIgnoringCase("otp auth url cannot be blank"); + assertThat(errorMessage).containsIgnoringCase("authUrl must not be blank"); totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/T B: aba"); errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) .andExpect(status().isBadRequest())); - assertThat(errorMessage).containsIgnoringCase("otp auth url is invalid"); + assertThat(errorMessage).containsIgnoringCase("authUrl is invalid"); totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/ThingsBoard%20(Tenant):tenant@thingsboard.org?issuer=ThingsBoard+%28Tenant%29&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII"); doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig) @@ -336,14 +336,14 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig) .andExpect(status().isBadRequest())); - assertThat(errorMessage).containsIgnoringCase("phone number cannot be blank"); + assertThat(errorMessage).containsIgnoringCase("phoneNumber must not be blank"); String nonE164PhoneNumber = "8754868"; smsTwoFaAccountConfig.setPhoneNumber(nonE164PhoneNumber); errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig) .andExpect(status().isBadRequest())); - assertThat(errorMessage).containsIgnoringCase("phone number is not of E.164 format"); + assertThat(errorMessage).containsIgnoringCase("phoneNumber is not of E.164 format"); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java new file mode 100644 index 0000000000..5aa50c9336 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/HomePageApiSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.sql; + +import org.thingsboard.server.controller.BaseHomePageApiTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class HomePageApiSqlTest extends BaseHomePageApiTest { +} diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 2729f8ec7a..d650e619a9 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -18,31 +18,27 @@ package org.thingsboard.server.service.apiusage; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; 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.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; @RunWith(MockitoJUnitRunner.class) public class DefaultTbApiUsageStateServiceTest { @@ -68,11 +64,12 @@ public class DefaultTbApiUsageStateServiceTest { TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); + @Spy + @InjectMocks DefaultTbApiUsageStateService service; @Before public void setUp() { - service = spy(new DefaultTbApiUsageStateService(clusterService, partitionService, tenantService, tsService, apiUsageStateService, tenantProfileCache, mailService, dbExecutor)); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index 93aeb17de6..decefb6f90 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -67,7 +67,7 @@ public class DefaultTbAlarmServiceTest { @MockBean protected AlarmService alarmService; @MockBean - protected AlarmCommentService alarmCommentService; + protected TbAlarmCommentService alarmCommentService; @MockBean protected AlarmSubscriptionService alarmSubscriptionService; @MockBean @@ -101,7 +101,7 @@ public class DefaultTbAlarmServiceTest { .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).build()); service.ack(alarm, new User(new UserId(UUID.randomUUID()))); - verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); + verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong()); } @@ -114,7 +114,7 @@ public class DefaultTbAlarmServiceTest { .thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).build()); service.clear(alarm, new User(new UserId(UUID.randomUUID()))); - verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); + verify(alarmCommentService, times(1)).saveAlarmComment(any(), any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any()); } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java new file mode 100644 index 0000000000..7bbd88445f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -0,0 +1,214 @@ +/** + * 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.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.util.Pair; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.slack.SlackService; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.id.UserId; +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.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationType; +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.platform.PlatformUsersNotificationTargetConfig; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.DaoUtil; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class AbstractNotificationApiTest extends AbstractControllerTest { + + protected NotificationApiWsClient wsClient; + protected NotificationApiWsClient otherWsClient; + + @MockBean + protected SlackService slackService; + + @Autowired + protected MailService mailService; + + public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test"; + public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL; + + protected NotificationTarget createNotificationTarget(UserId... usersIds) { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName("Users " + List.of(usersIds)); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + UserListFilter filter = new UserListFilter(); + filter.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds))); + targetConfig.setUsersFilter(filter); + notificationTarget.setConfiguration(targetConfig); + return saveNotificationTarget(notificationTarget); + } + + protected NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) { + return doPost("/api/notification/target", notificationTarget, NotificationTarget.class); + } + + protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, NotificationDeliveryMethod... deliveryMethods) { + return submitNotificationRequest(targetId, text, 0, deliveryMethods); + } + + protected NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) { + return submitNotificationRequest(List.of(targetId), text, delayInSec, deliveryMethods); + } + + protected NotificationRequest submitNotificationRequest(List targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) { + if (deliveryMethods.length == 0) { + deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB}; + } + NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods); + return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec); + } + + protected NotificationRequest submitNotificationRequest(List targets, NotificationTemplateId notificationTemplateId, int delayInSec) { + NotificationRequestConfig config = new NotificationRequestConfig(); + config.setSendingDelayInSec(delayInSec); + UserOriginatedNotificationInfo notificationInfo = new UserOriginatedNotificationInfo(); + notificationInfo.setDescription("My description"); + NotificationRequest notificationRequest = NotificationRequest.builder() + .targets(targets.stream().map(UUIDBased::getId).collect(Collectors.toList())) + .templateId(notificationTemplateId) + .info(notificationInfo) + .additionalConfig(config) + .build(); + return doPost("/api/notification/request", notificationRequest, NotificationRequest.class); + } + + protected NotificationRequestStats getStats(NotificationRequestId notificationRequestId) throws Exception { + return findNotificationRequest(notificationRequestId).getStats(); + } + + protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, + String text, NotificationDeliveryMethod... deliveryMethods) { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setTenantId(tenantId); + notificationTemplate.setName("Notification template: " + text); + notificationTemplate.setNotificationType(notificationType); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case WEB: { + WebDeliveryMethodNotificationTemplate template = new WebDeliveryMethodNotificationTemplate(); + template.setSubject(subject); + deliveryMethodNotificationTemplate = template; + break; + } + case EMAIL: { + EmailDeliveryMethodNotificationTemplate template = new EmailDeliveryMethodNotificationTemplate(); + template.setSubject(subject); + deliveryMethodNotificationTemplate = template; + break; + } + case SMS: { + deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + break; + } + default: + throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); + } + deliveryMethodNotificationTemplate.setEnabled(true); + deliveryMethodNotificationTemplate.setBody(text); + config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); + } + notificationTemplate.setConfiguration(config); + return saveNotificationTemplate(notificationTemplate); + } + + protected NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) { + return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class); + } + + protected void saveNotificationSettings(NotificationSettings notificationSettings) throws Exception { + doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk()); + } + + protected Pair createUserAndConnectWsClient(Authority authority) throws Exception { + User user = new User(); + user.setTenantId(tenantId); + user.setAuthority(authority); + user.setEmail(RandomStringUtils.randomAlphabetic(20) + "@thingsboard.com"); + user = createUserAndLogin(user, "12345678"); + NotificationApiWsClient wsClient = buildAndConnectWebSocketClient(); + return Pair.of(user, wsClient); + } + + protected NotificationRequestInfo findNotificationRequest(NotificationRequestId id) throws Exception { + return doGet("/api/notification/request/" + id, NotificationRequestInfo.class); + } + + protected PageData findNotificationRequests() throws Exception { + PageLink pageLink = new PageLink(10); + return doGetTypedWithPageLink("/api/notification/requests?", new TypeReference>() {}, pageLink); + } + + protected void deleteNotificationRequest(NotificationRequestId id) throws Exception { + doDelete("/api/notification/request/" + id); + } + + protected List getMyNotifications(boolean unreadOnly, int limit) throws Exception { + return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference>() {}, + new PageLink(limit, 0), unreadOnly).getData(); + } + + @Override + protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { + NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token); + assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue(); + return wsClient; + } + + @Override + public NotificationApiWsClient getWsClient() { + return (NotificationApiWsClient) super.getWsClient(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java new file mode 100644 index 0000000000..08ac826c51 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -0,0 +1,593 @@ +/** + * 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.extern.slf4j.Slf4j; +import org.assertj.core.data.Offset; +import org.java_websocket.client.WebSocketClient; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.UserId; +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.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.NotificationRequestStats; +import org.thingsboard.server.common.data.notification.NotificationRequestStatus; +import org.thingsboard.server.common.data.notification.NotificationType; +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.settings.SlackNotificationDeliveryMethodConfig; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +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.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; +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.EmailDeliveryMethodNotificationTemplate; +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 org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.notification.NotificationDao; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@DaoSqlTest +@Slf4j +public class NotificationApiTest extends AbstractNotificationApiTest { + + @Autowired + private NotificationCenter notificationCenter; + @Autowired + private NotificationDao notificationDao; + @Autowired + private DbCallbackExecutorService executor; + + @Before + public void beforeEach() throws Exception { + loginCustomerUser(); + wsClient = getWsClient(); + loginTenantAdmin(); + } + + @Test + public void testSubscribingToUnreadNotificationsCount() { + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText1 = "Notification 1"; + submitNotificationRequest(notificationTarget.getId(), notificationText1); + String notificationText2 = "Notification 2"; + submitNotificationRequest(notificationTarget.getId(), notificationText2); + + wsClient.subscribeForUnreadNotificationsCount(); + wsClient.waitForReply(true); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> wsClient.getLastCountUpdate().getTotalUnreadCount() == 2); + } + + @Test + public void testReceivingCountUpdates_multipleSessions() throws Exception { + connectOtherWsClient(); + wsClient.subscribeForUnreadNotificationsCount(); + otherWsClient.subscribeForUnreadNotificationsCount(); + wsClient.waitForReply(true); + otherWsClient.waitForReply(true); + assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isZero(); + + wsClient.registerWaitForUpdate(); + otherWsClient.registerWaitForUpdate(); + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText = "Notification"; + submitNotificationRequest(notificationTarget.getId(), notificationText); + wsClient.waitForUpdate(true); + otherWsClient.waitForUpdate(true); + + assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isOne(); + assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne(); + } + + @Test + public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception { + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText1 = "Notification 1"; + submitNotificationRequest(notificationTarget.getId(), notificationText1); + String notificationText2 = "Notification 2"; + submitNotificationRequest(notificationTarget.getId(), notificationText2); + + connectOtherWsClient(); + wsClient.subscribeForUnreadNotifications(10); + otherWsClient.subscribeForUnreadNotifications(10); + wsClient.waitForReply(true); + otherWsClient.waitForReply(true); + + checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText1, notificationText2); + checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText1, notificationText2); + } + + @Test + public void testReceivingNotificationUpdates_multipleSessions() throws Exception { + connectOtherWsClient(); + wsClient.subscribeForUnreadNotifications(10).waitForReply(true); + otherWsClient.subscribeForUnreadNotifications(10).waitForReply(true); + UnreadNotificationsUpdate notificationsUpdate = wsClient.getLastDataUpdate(); + assertThat(notificationsUpdate.getTotalUnreadCount()).isZero(); + + wsClient.registerWaitForUpdate(); + otherWsClient.registerWaitForUpdate(); + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText = "Notification 1"; + submitNotificationRequest(notificationTarget.getId(), notificationText); + wsClient.waitForUpdate(true); + otherWsClient.waitForUpdate(true); + + checkPartialNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText, 1); + checkPartialNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText, 1); + } + + @Test + public void testMarkingAsRead_multipleSessions() throws Exception { + connectOtherWsClient(); + wsClient.subscribeForUnreadNotifications(10).waitForReply(true); + otherWsClient.subscribeForUnreadNotifications(10).waitForReply(true); + + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + wsClient.registerWaitForUpdate(); + otherWsClient.registerWaitForUpdate(); + String notificationText1 = "Notification 1"; + submitNotificationRequest(notificationTarget.getId(), notificationText1); + wsClient.waitForUpdate(true); + otherWsClient.waitForUpdate(true); + Notification notification1 = wsClient.getLastDataUpdate().getUpdate(); + + wsClient.registerWaitForUpdate(); + otherWsClient.registerWaitForUpdate(); + String notificationText2 = "Notification 2"; + submitNotificationRequest(notificationTarget.getId(), notificationText2); + wsClient.waitForUpdate(true); + otherWsClient.waitForUpdate(true); + assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2); + assertThat(otherWsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2); + + wsClient.registerWaitForUpdate(); + otherWsClient.registerWaitForUpdate(); + wsClient.markNotificationAsRead(notification1.getUuidId()); + wsClient.waitForUpdate(true); + otherWsClient.waitForUpdate(true); + + checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText2); + checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText2); + } + + @Test + public void testMarkingAllAsRead() { + wsClient.subscribeForUnreadNotifications(10).waitForReply(true); + NotificationTarget target = createNotificationTarget(customerUserId); + int notificationsCount = 20; + wsClient.registerWaitForUpdate(notificationsCount); + for (int i = 1; i <= notificationsCount; i++) { + submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB); + } + wsClient.waitForUpdate(true); + assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount); + + wsClient.registerWaitForUpdate(1); + wsClient.markAllNotificationsAsRead(); + wsClient.waitForUpdate(true); + + assertThat(wsClient.getLastDataUpdate().getNotifications()).isEmpty(); + assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isZero(); + } + + @Test + public void testDelayedNotificationRequest() throws Exception { + wsClient.subscribeForUnreadNotifications(5); + wsClient.waitForReply(true); + + wsClient.registerWaitForUpdate(); + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText = "Was scheduled for 5 sec"; + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5); + assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED); + await().atLeast(4, TimeUnit.SECONDS) + .atMost(6, TimeUnit.SECONDS) + .until(() -> wsClient.getLastMsg() != null); + + Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate(); + assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText); + assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime()) + .isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(500L)); + assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.SENT); + } + + @Test + public void whenNotificationRequestIsDeleted_thenDeleteNotifications() throws Exception { + wsClient.subscribeForUnreadNotifications(10); + wsClient.waitForReply(true); + + wsClient.registerWaitForUpdate(); + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test"); + wsClient.waitForUpdate(true); + assertThat(wsClient.getNotifications()).singleElement().extracting(Notification::getRequestId) + .isEqualTo(notificationRequest.getId()); + assertThat(wsClient.getUnreadCount()).isOne(); + + wsClient.registerWaitForUpdate(); + deleteNotificationRequest(notificationRequest.getId()); + wsClient.waitForUpdate(true); + + assertThat(wsClient.getNotifications()).isEmpty(); + assertThat(wsClient.getUnreadCount()).isZero(); + loginCustomerUser(); + assertThat(getMyNotifications(false, 10)).size().isZero(); + } + + @Test + public void testNotificationUpdatesForSeveralUsers() throws Exception { + int usersCount = 150; + Map sessions = new HashMap<>(); + List targets = new ArrayList<>(); + + for (int i = 1; i <= usersCount; i++) { + User user = new User(); + user.setTenantId(tenantId); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail("test-user-" + i + "@thingsboard.org"); + user = createUserAndLogin(user, "12345678"); + NotificationApiWsClient wsClient = buildAndConnectWebSocketClient(); + sessions.put(user, wsClient); + + NotificationTarget notificationTarget = createNotificationTarget(user.getId()); + targets.add(notificationTarget.getId()); + + wsClient.registerWaitForUpdate(); + wsClient.subscribeForUnreadNotifications(10); + } + sessions.values().forEach(wsClient -> wsClient.waitForUpdate(true)); + + loginTenantAdmin(); + + sessions.forEach((user, wsClient) -> wsClient.registerWaitForUpdate()); + NotificationRequest notificationRequest = submitNotificationRequest(targets, "Hello, ${recipientEmail}", 0, + NotificationDeliveryMethod.WEB); + await().atMost(10, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS) + .until(() -> { + long receivedUpdate = sessions.values().stream() + .filter(wsClient -> wsClient.getLastDataUpdate() != null) + .count(); + System.err.println("WS sessions received update: " + receivedUpdate); + return receivedUpdate == sessions.size(); + }); + + sessions.forEach((user, wsClient) -> { + assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isOne(); + + Notification notification = wsClient.getLastDataUpdate().getUpdate(); + assertThat(notification.getRecipientId()).isEqualTo(user.getId()); + assertThat(notification.getRequestId()).isEqualTo(notificationRequest.getId()); + assertThat(notification.getText()).isEqualTo("Hello, " + user.getEmail()); + }); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); + NotificationRequestStats stats = getStats(notificationRequest.getId()); + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(usersCount); + + sessions.values().forEach(wsClient -> wsClient.registerWaitForUpdate()); + deleteNotificationRequest(notificationRequest.getId()); + sessions.values().forEach(wsClient -> { + wsClient.waitForUpdate(true); + assertThat(wsClient.getLastDataUpdate().getNotifications()).isEmpty(); + assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isZero(); + }); + + sessions.values().forEach(WebSocketClient::close); + } + + @Test + public void testNotificationRequestPreview() throws Exception { + NotificationTarget target1 = new NotificationTarget(); + target1.setName("Me"); + PlatformUsersNotificationTargetConfig target1Config = new PlatformUsersNotificationTargetConfig(); + UserListFilter userListFilter = new UserListFilter(); + userListFilter.setUsersIds(DaoUtil.toUUIDs(List.of(tenantAdminUserId))); + target1Config.setUsersFilter(userListFilter); + target1.setConfiguration(target1Config); + target1 = saveNotificationTarget(target1); + List recipients = new ArrayList<>(); + recipients.add(tenantAdminUserId); + + createDifferentCustomer(); + loginTenantAdmin(); + int customerUsersCount = 10; + for (int i = 0; i < customerUsersCount; i++) { + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(tenantId); + customerUser.setCustomerId(differentCustomerId); + customerUser.setEmail("other-customer-" + i + "@thingsboard.org"); + customerUser = createUser(customerUser, "12345678"); + recipients.add(customerUser.getId()); + } + NotificationTarget target2 = new NotificationTarget(); + target2.setName("Other customer users"); + PlatformUsersNotificationTargetConfig target2Config = new PlatformUsersNotificationTargetConfig(); + CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter(); + customerUsersFilter.setCustomerId(differentCustomerId.getId()); + target2Config.setUsersFilter(customerUsersFilter); + target2.setConfiguration(target2Config); + target2 = saveNotificationTarget(target2); + + + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setNotificationType(NotificationType.GENERAL); + notificationTemplate.setName("Test template"); + + String requestorEmail = TENANT_ADMIN_EMAIL; + NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); + HashMap templates = new HashMap<>(); + templateConfig.setDeliveryMethodsTemplates(templates); + notificationTemplate.setConfiguration(templateConfig); + + WebDeliveryMethodNotificationTemplate webNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); + webNotificationTemplate.setEnabled(true); + webNotificationTemplate.setBody("Message for WEB: ${recipientEmail}"); + webNotificationTemplate.setSubject("Subject for WEB: ${recipientEmail}"); + templates.put(NotificationDeliveryMethod.WEB, webNotificationTemplate); + + SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + smsNotificationTemplate.setEnabled(true); + smsNotificationTemplate.setBody("Message for SMS: ${recipientEmail}"); + templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate); + + EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailNotificationTemplate.setEnabled(true); + emailNotificationTemplate.setSubject("Subject for EMAIL: ${recipientEmail}"); + emailNotificationTemplate.setBody("Message for EMAIL: ${recipientEmail}"); + templates.put(NotificationDeliveryMethod.EMAIL, emailNotificationTemplate); + + SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate(); + slackNotificationTemplate.setEnabled(true); + slackNotificationTemplate.setBody("Message for SLACK: ${recipientEmail}"); + templates.put(NotificationDeliveryMethod.SLACK, slackNotificationTemplate); + + notificationTemplate = saveNotificationTemplate(notificationTemplate); + + + NotificationRequest notificationRequest = new NotificationRequest(); + notificationRequest.setTargets(List.of(target1.getUuidId(), target2.getUuidId())); + notificationRequest.setTemplateId(notificationTemplate.getId()); + notificationRequest.setAdditionalConfig(new NotificationRequestConfig()); + + NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class); + assertThat(preview.getRecipientsCountByTarget().get(target1.getName())).isEqualTo(1); + assertThat(preview.getRecipientsCountByTarget().get(target2.getName())).isEqualTo(customerUsersCount); + assertThat(preview.getTotalRecipientsCount()).isEqualTo(1 + customerUsersCount); + assertThat(preview.getRecipientsPreview()).extracting(User::getId).containsAll(recipients); + + Map processedTemplates = preview.getProcessedTemplates(); + assertThat(processedTemplates.get(NotificationDeliveryMethod.WEB)).asInstanceOf(type(WebDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .startsWith("Message for WEB") + .endsWith(requestorEmail); + assertThat(template.getSubject()) + .startsWith("Subject for WEB") + .endsWith(requestorEmail); + }); + assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .startsWith("Message for SMS") + .endsWith(requestorEmail); + }); + assertThat(processedTemplates.get(NotificationDeliveryMethod.EMAIL)).asInstanceOf(type(EmailDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .startsWith("Message for EMAIL") + .endsWith(requestorEmail); + assertThat(template.getSubject()) + .startsWith("Subject for EMAIL") + .endsWith(requestorEmail); + }); + assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .isEqualTo("Message for SLACK: "); // ${recipientEmail} should be removed + }); + } + + @Test + public void testNotificationRequestInfo() throws Exception { + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.WEB + }; + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods); + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + NotificationRequest request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0); + + NotificationRequestInfo requestInfo = findNotificationRequests().getData().get(0); + assertThat(requestInfo.getId()).isEqualTo(request.getId()); + assertThat(requestInfo.getTemplateName()).isEqualTo(template.getName()); + assertThat(requestInfo.getDeliveryMethods()).containsOnly(deliveryMethods); + } + + @Test + public void testNotificationRequestStats() throws Exception { + wsClient.subscribeForUnreadNotifications(10); + wsClient.waitForReply(true); + + wsClient.registerWaitForUpdate(); + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test :)", NotificationDeliveryMethod.WEB); + wsClient.waitForUpdate(); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); + NotificationRequestStats stats = getStats(notificationRequest.getId()); + + assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); + } + + @Test + public void testNotificationsForALotOfUsers() throws Exception { + int usersCount = 5000; + + List users = new ArrayList<>(); + for (int i = 1; i <= usersCount; i++) { + User user = new User(); + user.setTenantId(tenantId); + user.setAuthority(Authority.TENANT_ADMIN); + user.setEmail("test-user-" + i + "@thingsboard.org"); + user = doPost("/api/user", user, User.class); + users.add(user); + } + + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName("All my users"); + PlatformUsersNotificationTargetConfig config = new PlatformUsersNotificationTargetConfig(); + AllUsersFilter filter = new AllUsersFilter(); + config.setUsersFilter(filter); + notificationTarget.setConfiguration(config); + notificationTarget = saveNotificationTarget(notificationTarget); + NotificationTargetId notificationTargetId = notificationTarget.getId(); + + ListenableFuture request = executor.submit(() -> { + return submitNotificationRequest(notificationTargetId, "Hello, ${recipientEmail}", 0, NotificationDeliveryMethod.WEB); + }); + await().atMost(10, TimeUnit.SECONDS).until(request::isDone); + NotificationRequest notificationRequest = request.get(); + + await().atMost(5, TimeUnit.SECONDS) + .pollInterval(200, TimeUnit.MILLISECONDS) + .until(() -> { + PageData sentNotifications = notificationDao.findByRequestId(tenantId, notificationRequest.getId(), new PageLink(1)); + return sentNotifications.getTotalElements() >= usersCount; + }); + + PageData sentNotifications = notificationDao.findByRequestId(tenantId, notificationRequest.getId(), new PageLink(Integer.MAX_VALUE)); + assertThat(sentNotifications.getData()).extracting(Notification::getRecipientId) + .containsAll(users.stream().map(User::getId).collect(Collectors.toSet())); + + NotificationRequestStats stats = getStats(notificationRequest.getId()); + assertThat(stats.getSent().values().stream().mapToInt(AtomicInteger::get).sum()).isGreaterThanOrEqualTo(usersCount); + } + + @Test + public void testSlackNotifications() throws Exception { + NotificationSettings settings = new NotificationSettings(); + SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig(); + String slackToken = "xoxb-123123123"; + slackConfig.setBotToken(slackToken); + settings.setDeliveryMethodsConfigs(Map.of( + NotificationDeliveryMethod.SLACK, slackConfig + )); + saveNotificationSettings(settings); + + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setName("Slack notification template"); + notificationTemplate.setNotificationType(NotificationType.GENERAL); + NotificationTemplateConfig config = new NotificationTemplateConfig(); + SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate(); + slackNotificationTemplate.setEnabled(true); + slackNotificationTemplate.setBody("To Slack :) ${recipientEmail}"); + config.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.SLACK, slackNotificationTemplate + )); + notificationTemplate.setConfiguration(config); + notificationTemplate = saveNotificationTemplate(notificationTemplate); + + String conversationId = "U154475415"; + String conversationName = "#my-channel"; + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName(conversationName + " in Slack"); + SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig(); + targetConfig.setConversation(new SlackConversation(conversationId, conversationName)); + notificationTarget.setConfiguration(targetConfig); + notificationTarget = saveNotificationTarget(notificationTarget); + + NotificationRequest successfulNotificationRequest = submitNotificationRequest(List.of(notificationTarget.getId()), notificationTemplate.getId(), 0); + await().atMost(2, TimeUnit.SECONDS) + .until(() -> findNotificationRequest(successfulNotificationRequest.getId()).isSent()); + verify(slackService).sendMessage(eq(tenantId), eq(slackToken), eq(conversationId), eq("To Slack :) ")); + NotificationRequestStats stats = getStats(successfulNotificationRequest.getId()); + assertThat(stats.getSent().get(NotificationDeliveryMethod.SLACK)).hasValue(1); + + String errorMessage = "Error!!!"; + doThrow(new RuntimeException(errorMessage)).when(slackService).sendMessage(any(), any(), any(), any()); + NotificationRequest failedNotificationRequest = submitNotificationRequest(List.of(notificationTarget.getId()), notificationTemplate.getId(), 0); + await().atMost(2, TimeUnit.SECONDS) + .until(() -> findNotificationRequest(failedNotificationRequest.getId()).isSent()); + stats = getStats(failedNotificationRequest.getId()); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage); + } + + private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { + assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications); + assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE); + assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getSubject).containsOnly(DEFAULT_NOTIFICATION_SUBJECT); + assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedNotifications.length); + } + + private void checkPartialNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String expectedNotification, int expectedUnreadCount) { + assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getText).isEqualTo(expectedNotification); + assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getType).isEqualTo(DEFAULT_NOTIFICATION_TYPE); + assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getSubject).isEqualTo(DEFAULT_NOTIFICATION_SUBJECT); + assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedUnreadCount); + } + + protected void connectOtherWsClient() throws Exception { + loginCustomerUser(); + otherWsClient = (NotificationApiWsClient) super.getAnotherWsClient(); + loginTenantAdmin(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java new file mode 100644 index 0000000000..664ff7bd54 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiWsClient.java @@ -0,0 +1,133 @@ +/** + * 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 lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.controller.TbTestWebSocketClient; +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.NotificationCmdsWrapper; +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.cmd.UnreadNotificationsCountUpdate; +import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; +import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Getter +public class NotificationApiWsClient extends TbTestWebSocketClient { + + private UnreadNotificationsUpdate lastDataUpdate; + private UnreadNotificationsCountUpdate lastCountUpdate; + + private int limit; + private int unreadCount; + private List notifications; + + public NotificationApiWsClient(String wsUrl, String token) throws URISyntaxException { + super(new URI(wsUrl + "/api/ws/plugins/notifications?token=" + token)); + } + + public NotificationApiWsClient subscribeForUnreadNotifications(int limit) { + NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); + cmdsWrapper.setUnreadSubCmd(new NotificationsSubCmd(1, limit)); + sendCmd(cmdsWrapper); + this.limit = limit; + return this; + } + + public NotificationApiWsClient subscribeForUnreadNotificationsCount() { + NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); + cmdsWrapper.setUnreadCountSubCmd(new NotificationsCountSubCmd(2)); + sendCmd(cmdsWrapper); + return this; + } + + public void markNotificationAsRead(UUID... notifications) { + NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); + cmdsWrapper.setMarkAsReadCmd(new MarkNotificationsAsReadCmd(newCmdId(), Arrays.asList(notifications))); + sendCmd(cmdsWrapper); + } + + public void markAllNotificationsAsRead() { + NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); + cmdsWrapper.setMarkAllAsReadCmd(new MarkAllNotificationsAsReadCmd(newCmdId())); + sendCmd(cmdsWrapper); + } + + public void sendCmd(NotificationCmdsWrapper cmdsWrapper) { + String cmd = JacksonUtil.toString(cmdsWrapper); + send(cmd); + } + + @Override + public void registerWaitForUpdate(int count) { + lastDataUpdate = null; + lastCountUpdate = null; + super.registerWaitForUpdate(count); + } + + @Override + public void onMessage(String s) { + JsonNode update = JacksonUtil.toJsonNode(s); + CmdUpdateType updateType = CmdUpdateType.valueOf(update.get("cmdUpdateType").asText()); + if (updateType == CmdUpdateType.NOTIFICATIONS) { + lastDataUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsUpdate.class); + unreadCount = lastDataUpdate.getTotalUnreadCount(); + if (lastDataUpdate.getNotifications() != null) { + notifications = new ArrayList<>(lastDataUpdate.getNotifications()); + } else { + Notification notificationUpdate = lastDataUpdate.getUpdate(); + boolean updated = false; + for (int i = 0; i < notifications.size(); i++) { + Notification existing = notifications.get(i); + if (existing.getId().equals(notificationUpdate.getId())) { + notifications.set(i, notificationUpdate); + updated = true; + break; + } + } + if (!updated) { + notifications.add(0, notificationUpdate); + if (notifications.size() > limit) { + notifications = notifications.subList(0, limit); + } + } + } + } else if (updateType == CmdUpdateType.NOTIFICATIONS_COUNT) { + lastCountUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsCountUpdate.class); + unreadCount = lastCountUpdate.getTotalUnreadCount(); + } + super.onMessage(s); + } + + private static int newCmdId() { + return RandomUtils.nextInt(1, 1000); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java new file mode 100644 index 0000000000..fdcb5e754c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -0,0 +1,438 @@ +/** + * 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.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.data.util.Pair; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +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.device.profile.AlarmCondition; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.id.NotificationRuleId; +import org.thingsboard.server.common.data.id.RuleChainId; +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.NotificationType; +import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; +import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; +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.EntityActionNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +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.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.query.BooleanFilterPredicate; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.script.ScriptLanguage; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.offset; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@TestPropertySource(properties = { + "js.evaluator=local" +}) +public class NotificationRuleApiTest extends AbstractNotificationApiTest { + + @SpyBean + private AlarmSubscriptionService alarmSubscriptionService; + @Autowired + private NotificationRequestService notificationRequestService; + @Autowired + private NotificationRuleService notificationRuleService; + @Autowired + private NotificationTemplateService notificationTemplateService; + + @SpyBean + private AlarmService alarmService; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + notificationRuleService.deleteNotificationRulesByTenantId(tenantId); + notificationTemplateService.deleteNotificationTemplatesByTenantId(tenantId); + } + + @Test + public void testNotificationRuleProcessing_entityActionTrigger() throws Exception { + String notificationSubject = "${actionType}: ${entityType} [${entityId}]"; + String notificationText = "User: ${originatorUserName}"; + NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.GENERAL, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Web notification when any device is created, updated or deleted"); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + + EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); + triggerConfig.setEntityType(EntityType.DEVICE); + triggerConfig.setCreated(true); + triggerConfig.setUpdated(true); + triggerConfig.setDeleted(true); + + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); + + notificationRule.setTriggerConfig(triggerConfig); + notificationRule.setRecipientsConfig(recipientsConfig); + notificationRule = saveNotificationRule(notificationRule); + + getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + + + getWsClient().registerWaitForUpdate(); + Device device = createDevice("DEVICE!!!", "default", "12345"); + getWsClient().waitForUpdate(true); + + Notification notification = getWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo("added: Device [" + device.getId() + "]"); + assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL); + + + getWsClient().registerWaitForUpdate(); + device.setName("Updated name"); + device = doPost("/api/device", device, Device.class); + getWsClient().waitForUpdate(true); + + notification = getWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]"); + + + getWsClient().registerWaitForUpdate(); + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + getWsClient().waitForUpdate(true); + + notification = getWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]"); + } + + @Test + public void testNotificationRuleProcessing_alarmTrigger() throws Exception { + String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}"; + String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; + NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Web notification on any alarm"); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); + + AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); + triggerConfig.setAlarmTypes(null); + triggerConfig.setAlarmSeverities(null); + triggerConfig.setNotifyOn(Set.of(AlarmAction.CREATED, AlarmAction.SEVERITY_CHANGED, AlarmAction.ACKNOWLEDGED, AlarmAction.CLEARED)); + notificationRule.setTriggerConfig(triggerConfig); + + EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); + Map> escalationTable = new HashMap<>(); + recipientsConfig.setEscalationTable(escalationTable); + Map clients = new HashMap<>(); + for (int delay = 0; delay <= 5; delay++) { + Pair userAndClient = createUserAndConnectWsClient(Authority.TENANT_ADMIN); + NotificationTarget notificationTarget = createNotificationTarget(userAndClient.getFirst().getId()); + escalationTable.put(delay, List.of(notificationTarget.getUuidId())); + clients.put(delay, userAndClient.getSecond()); + } + notificationRule.setRecipientsConfig(recipientsConfig); + notificationRule = saveNotificationRule(notificationRule); + + + String alarmType = "myBoolIsTrue"; + DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); + Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); + + clients.values().forEach(wsClient -> { + wsClient.subscribeForUnreadNotifications(10).waitForReply(true); + wsClient.registerWaitForUpdate(); + }); + + JsonNode attr = JacksonUtil.newObjectNode() + .set("bool", BooleanNode.TRUE); + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null); + Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get(); + + long ts = System.currentTimeMillis(); + await().atMost(7, TimeUnit.SECONDS) + .until(() -> clients.values().stream().allMatch(client -> client.getLastDataUpdate() != null)); + clients.forEach((expectedDelay, wsClient) -> { + Notification notification = wsClient.getLastDataUpdate().getUpdate(); + double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000; + assertThat(actualDelay).isCloseTo(expectedDelay, offset(0.5)); + + assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + + "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); + assertThat(notification.getText()).isEqualTo("Status: " + AlarmStatus.ACTIVE_UNACK + ", severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase()); + + assertThat(notification.getType()).isEqualTo(NotificationType.ALARM); + assertThat(notification.getInfo()).isInstanceOf(AlarmNotificationInfo.class); + AlarmNotificationInfo info = (AlarmNotificationInfo) notification.getInfo(); + assertThat(info.getAlarmId()).isEqualTo(alarm.getUuidId()); + assertThat(info.getAlarmType()).isEqualTo(alarmType); + assertThat(info.getAlarmSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(info.getAlarmStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + clients.values().forEach(wsClient -> wsClient.registerWaitForUpdate()); + alarmSubscriptionService.acknowledgeAlarm(tenantId, alarm.getId(), System.currentTimeMillis()); + AlarmStatus expectedStatus = AlarmStatus.ACTIVE_ACK; + AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL; + clients.values().forEach(wsClient -> { + wsClient.waitForUpdate(true); + Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate(); + assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + + "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId()); + assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity.toString().toLowerCase()); + + wsClient.close(); + }); + } + + @Test + public void testNotificationRuleProcessing_alarmTrigger_clearRule() throws Exception { + String notificationSubject = "${alarmSeverity} alarm '${alarmType}' is ${alarmStatus}"; + String notificationText = "${alarmId}"; + NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Web notification on any alarm"); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); + + String alarmType = "myBoolIsTrue"; + DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType); + Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); + + AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); + triggerConfig.setAlarmTypes(Set.of(alarmType)); + triggerConfig.setAlarmSeverities(null); + triggerConfig.setNotifyOn(Set.of(AlarmAction.CREATED, AlarmAction.SEVERITY_CHANGED, AlarmAction.ACKNOWLEDGED)); + + AlarmNotificationRuleTriggerConfig.ClearRule clearRule = new AlarmNotificationRuleTriggerConfig.ClearRule(); + clearRule.setAlarmStatuses(Set.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.UNACK)); + triggerConfig.setClearRule(clearRule); + notificationRule.setTriggerConfig(triggerConfig); + + EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ALARM); + Map> escalationTable = new HashMap<>(); + recipientsConfig.setEscalationTable(escalationTable); + + escalationTable.put(0, List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); + escalationTable.put(1000, List.of(createNotificationTarget(customerUserId).getUuidId())); + + notificationRule.setRecipientsConfig(recipientsConfig); + notificationRule = saveNotificationRule(notificationRule); + + getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + getWsClient().registerWaitForUpdate(); + JsonNode attr = JacksonUtil.newObjectNode() + .set("bool", BooleanNode.TRUE); + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null); + Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get(); + getWsClient().waitForUpdate(true); + + Notification notification = getWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo("critical alarm '" + alarmType + "' is ACTIVE_UNACK"); + assertThat(notification.getInfo()).asInstanceOf(type(AlarmNotificationInfo.class)) + .extracting(AlarmNotificationInfo::getAlarmId).isEqualTo(alarm.getUuidId()); + + await().atMost(2, TimeUnit.SECONDS).until(() -> findNotificationRequests(EntityType.ALARM).getTotalElements() == escalationTable.size()); + NotificationRequestInfo scheduledNotificationRequest = findNotificationRequests(EntityType.ALARM).getData().stream() + .filter(NotificationRequest::isScheduled) + .findFirst().orElse(null); + assertThat(scheduledNotificationRequest).extracting(NotificationRequest::getInfo).isEqualTo(notification.getInfo()); + + getWsClient().registerWaitForUpdate(); + alarmSubscriptionService.clearAlarm(tenantId, alarm.getId(), System.currentTimeMillis(), null); + getWsClient().waitForUpdate(true); + notification = getWsClient().getLastDataUpdate().getUpdate(); + assertThat(notification.getSubject()).isEqualTo("critical alarm '" + alarmType + "' is CLEARED_UNACK"); + + assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); + } + + @Test + public void testNotificationRuleInfo() throws Exception { + NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL}; + NotificationTemplate template = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Subject", "Text", deliveryMethods); + + NotificationRule rule = new NotificationRule(); + rule.setName("Test"); + rule.setTemplateId(template.getId()); + + rule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); + rule.setTriggerConfig(triggerConfig); + + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); + recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); + rule.setRecipientsConfig(recipientsConfig); + rule = saveNotificationRule(rule); + + NotificationRuleInfo ruleInfo = findNotificationRules().getData().get(0); + assertThat(ruleInfo.getId()).isEqualTo(ruleInfo.getId()); + assertThat(ruleInfo.getTemplateName()).isEqualTo(template.getName()); + assertThat(ruleInfo.getDeliveryMethods()).containsOnly(deliveryMethods); + } + + private DeviceProfile createDeviceProfileWithAlarmRules(NotificationRuleId notificationRuleId, String alarmType) { + DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); + deviceProfile.setTenantId(tenantId); + + List alarms = new ArrayList<>(); + DeviceProfileAlarm alarm = new DeviceProfileAlarm(); + alarm.setAlarmType(alarmType); + alarm.setId(alarmType); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setAlarmDetails("Details"); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setSpec(new SimpleAlarmConditionSpec()); + List condition = new ArrayList<>(); + + AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); + alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "bool")); + BooleanFilterPredicate predicate = new BooleanFilterPredicate(); + predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + predicate.setValue(new FilterPredicateValue<>(true)); + + alarmConditionFilter.setPredicate(predicate); + alarmConditionFilter.setValueType(EntityKeyValueType.BOOLEAN); + condition.add(alarmConditionFilter); + alarmCondition.setCondition(condition); + alarmRule.setCondition(alarmCondition); + TreeMap createRules = new TreeMap<>(); + createRules.put(AlarmSeverity.CRITICAL, alarmRule); + alarm.setCreateRules(createRules); + alarms.add(alarm); + + deviceProfile.getProfileData().setAlarms(alarms); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + return deviceProfile; + } + + private RuleChain createEmptyRuleChain(String name) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(name); + ruleChain.setTenantId(tenantId); + ruleChain.setRoot(false); + ruleChain.setDebugMode(false); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChain.getId()); + metaData.setNodes(List.of()); + metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); + return ruleChain; + } + + private RuleNode addRuleNodeWithError(RuleChainId ruleChainId, String name) { + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode generatorNodeWithError = new RuleNode(); + generatorNodeWithError.setName(name); + generatorNodeWithError.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration generatorNodeConfiguration = new TbMsgGeneratorNodeConfiguration(); + generatorNodeConfiguration.setScriptLang(ScriptLanguage.JS); + generatorNodeConfiguration.setPeriodInSeconds(1000); + generatorNodeConfiguration.setMsgCount(1); + generatorNodeConfiguration.setJsScript("[return"); + generatorNodeWithError.setConfiguration(mapper.valueToTree(generatorNodeConfiguration)); + + metaData.setNodes(List.of(generatorNodeWithError)); + metaData.setFirstNodeIndex(0); + metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); + return metaData.getNodes().get(0); + } + + private NotificationRule saveNotificationRule(NotificationRule notificationRule) { + return doPost("/api/notification/rule", notificationRule, NotificationRule.class); + } + + private PageData findNotificationRules() throws Exception { + PageLink pageLink = new PageLink(10); + return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference>() {}, pageLink); + } + + private PageData findNotificationRequests(EntityType originatorType) { + return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java new file mode 100644 index 0000000000..61e0531a39 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java @@ -0,0 +1,164 @@ +/** + * 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.core.type.TypeReference; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +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.UserListFilter; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.notification.NotificationTargetDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class NotificationTargetApiTest extends AbstractControllerTest { + + @Autowired + private NotificationTargetDao notificationTargetDao; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + } + + @Test + public void givenInvalidNotificationTarget_whenSaving_returnValidationError() throws Exception { + NotificationTarget target = new NotificationTarget(); + target.setTenantId(null); + target.setName(null); + target.setConfiguration(null); + + String validationError = saveAndGetError(target, status().isBadRequest()); + assertThat(validationError) + .contains("name must not be") + .contains("configuration must not be"); + + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + UserListFilter userListFilter = new UserListFilter(); + userListFilter.setUsersIds(Collections.emptyList()); + targetConfig.setUsersFilter(userListFilter); + target.setConfiguration(targetConfig); + + validationError = saveAndGetError(target, status().isBadRequest()); + assertThat(validationError) + .contains("usersIds must not be"); + } + + @Test + public void givenNotificationTargetWithUsersFromDifferentTenant_whenSaving_returnAccessDeniedError() throws Exception { + loginDifferentTenant(); + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(differentTenantId); + notificationTarget.setName("Target 1"); + + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + UserListFilter userListFilter = new UserListFilter(); + userListFilter.setUsersIds(List.of(customerUserId.getId(), tenantAdminUserId.getId())); + targetConfig.setUsersFilter(userListFilter); + notificationTarget.setConfiguration(targetConfig); + + saveAndGetError(notificationTarget, status().isForbidden()); + + loginSysAdmin(); + notificationTarget.setTenantId(TenantId.SYS_TENANT_ID); + save(notificationTarget, status().isOk()); + } + + @Test + public void givenNotificationTargetConfig_testGetRecipients() throws Exception { + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setTenantId(tenantId); + notificationTarget.setName("Test target"); + + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + CustomerUsersFilter customerUsersFilter = new CustomerUsersFilter(); + customerUsersFilter.setCustomerId(customerId.getId()); + targetConfig.setUsersFilter(customerUsersFilter); + notificationTarget.setConfiguration(targetConfig); + + List recipients = getRecipients(notificationTarget); + assertThat(recipients).size().isNotZero(); + assertThat(recipients).allSatisfy(recipient -> { + assertThat(recipient.getCustomerId()).isEqualTo(customerId); + }); + + AllUsersFilter allUsersFilter = new AllUsersFilter(); + targetConfig.setUsersFilter(allUsersFilter); + recipients = getRecipients(notificationTarget); + assertThat(recipients).size().isGreaterThanOrEqualTo(2); + assertThat(recipients).allSatisfy(recipient -> { + assertThat(recipient.getTenantId()).isEqualTo(tenantId); + }); + + createDifferentTenant(); + loginSysAdmin(); + recipients = getRecipients(notificationTarget); + assertThat(recipients).size().isGreaterThanOrEqualTo(3); + assertThat(recipients).anySatisfy(recipient -> { + assertThat(recipient.getTenantId()).isEqualTo(tenantId); + }); + assertThat(recipients).anySatisfy(recipient -> { + assertThat(recipient.getTenantId()).isEqualTo(differentTenantId); + }); + } + + @Test + public void whenDeletingTenant_thenDeleteNotificationTarget() throws Exception { + createDifferentTenant(); + NotificationTarget notificationTarget = new NotificationTarget(); + notificationTarget.setName("Test 1"); + notificationTarget.setTenantId(differentTenantId); + PlatformUsersNotificationTargetConfig targetConfig = new PlatformUsersNotificationTargetConfig(); + targetConfig.setUsersFilter(new AllUsersFilter()); + notificationTarget.setConfiguration(targetConfig); + save(notificationTarget, status().isOk()); + assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isNotEmpty(); + + deleteDifferentTenant(); + assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty(); + } + + private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { + return getErrorMessage(save(notificationTarget, statusMatcher)); + } + + private ResultActions save(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { + return doPost("/api/notification/target", notificationTarget) + .andExpect(statusMatcher); + } + + private List getRecipients(NotificationTarget notificationTarget) throws Exception { + return doPostWithTypedResponse("/api/notification/target/recipients?page=0&pageSize=100", notificationTarget, new TypeReference>() {}).getData(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java new file mode 100644 index 0000000000..1b843d61b4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java @@ -0,0 +1,127 @@ +/** + * 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.core.type.TypeReference; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import org.thingsboard.server.common.data.id.IdBased; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationType; +import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.notification.NotificationRuleService; +import org.thingsboard.server.dao.notification.NotificationTemplateService; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class NotificationTemplateApiTest extends AbstractNotificationApiTest { + + @Autowired + private NotificationTemplateService templateService; + @Autowired + private NotificationRuleService notificationRuleService; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + notificationRuleService.deleteNotificationRulesByTenantId(tenantId); + templateService.deleteNotificationTemplatesByTenantId(tenantId); + } + + @Test + public void givenInvalidNotificationTemplate_whenSaving_returnValidationError() throws Exception { + NotificationTemplate notificationTemplate = new NotificationTemplate(); + notificationTemplate.setTenantId(tenantId); + notificationTemplate.setName(null); + notificationTemplate.setNotificationType(null); + notificationTemplate.setConfiguration(null); + + String validationError = saveAndGetError(notificationTemplate, status().isBadRequest()); + assertThat(validationError) + .contains("name must not be") + .contains("notificationType must not be") + .contains("configuration must not be"); + + NotificationTemplateConfig config = new NotificationTemplateConfig(); + notificationTemplate.setConfiguration(config); + EmailDeliveryMethodNotificationTemplate emailTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailTemplate.setEnabled(true); + emailTemplate.setBody(null); + emailTemplate.setSubject(null); + config.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.EMAIL, emailTemplate + )); + notificationTemplate.setName("