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 d71fc158ef..75cb42e1cf 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 @@ -89,6 +89,77 @@ 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); +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_created_time ON notification_rule(tenant_id, 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_originator_type_created_time ON notification_request(tenant_id, originator_entity_type, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_notification_request_rule_id_originator_entity_id ON notification_request(rule_id, originator_entity_id); +CREATE INDEX IF NOT EXISTS idx_notification_request_status ON notification_request(status); + +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), + text VARCHAR(1000) NOT NULL, + additional_config VARCHAR(1000), + info VARCHAR(1000), + status VARCHAR(32) +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_notification_id_recipient_id ON notification(id, recipient_id); +CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_status_created_time ON notification(recipient_id, status, created_time DESC); + +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), 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..532fc5090c 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; @@ -90,8 +92,10 @@ 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.notification.rule.NotificationRuleProcessingService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; @@ -307,6 +311,10 @@ public class ActorSystemContext { @Getter private ExternalCallExecutorService externalCallExecutorService; + @Autowired + @Getter + private NotificationExecutorService notificationExecutor; + @Autowired @Getter private SharedEventLoopGroupService sharedEventLoopGroupService; @@ -323,6 +331,18 @@ public class ActorSystemContext { @Getter private SmsSenderFactory smsSenderFactory; + @Autowired + @Getter + private NotificationCenter notificationCenter; + + @Autowired + @Getter + private NotificationRuleProcessingService notificationRuleProcessingService; + + @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..c3e18abbb8 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; @@ -472,6 +474,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 +693,16 @@ class DefaultTbContext implements TbContext { return mainCtx.getSmsSenderFactory(); } + @Override + public NotificationCenter getNotificationCenter() { + return mainCtx.getNotificationCenter(); + } + + @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); + systemContext.getNotificationRuleProcessingService().process(tenantId, getRuleChainId(), getRuleChainName(), + id, processor.getComponentName(), event, e); + } + + 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() + " with id [" + entityId + "] 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/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java new file mode 100644 index 0000000000..e4e37a7788 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -0,0 +1,297 @@ +/** + * 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.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.NotificationProcessingContext; +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.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; + +import javax.validation.Valid; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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, + @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 + 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) { + recipientsCount = notificationTargetService.countRecipientsForNotificationTargetConfig(user.getTenantId(), target.getConfiguration()); + } else { + recipientsCount = 1; + } + recipientsCountByTarget.put(target.getName(), recipientsCount); + } + 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) { + // generic permission + 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) { + // generic permission + TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); + return notificationSettingsService.findNotificationSettings(tenantId); + } + +} 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..8c08e1eab2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java @@ -0,0 +1,95 @@ +/** + * 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.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('TENANT_ADMIN')") + public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule) throws Exception { + notificationRule.setTenantId(getTenantId()); + checkEntity(notificationRule.getId(), notificationRule, NOTIFICATION); + return doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule); + } + + @GetMapping("/rule/{id}") + @PreAuthorize("hasAnyAuthority('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('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('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..b07cff0849 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java @@ -0,0 +1,174 @@ +/** + * 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.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.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.UsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.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); + } + + @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(); + if (usersFilter.getType() == UsersFilterType.USER_LIST) { + PageDataIterable recipients = new PageDataIterable<>(pageLink -> { + return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), null, targetConfig, pageLink); + }, 200); + for (User recipient : recipients) { + checkEntity(user, recipient, Operation.READ); + } + } else if (usersFilter.getType() == UsersFilterType.CUSTOMER_USERS) { + CustomerId customerId = new CustomerId(((CustomerUsersFilter) usersFilter).getCustomerId()); + checkEntityId(customerId, Operation.READ); + } + } + +} 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/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 c0f4bd5cf4..a22b8854bb 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -242,6 +242,7 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.4.4"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); + systemDataLoaderService.createDefaultNotificationConfigs(); break; //TODO update CacheCleanupService on the next version upgrade default: 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..00bbf3b9e5 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,7 @@ public class EntityActionService { if (entityId.getEntityType() == EntityType.DASHBOARD) { entityNode.put("configuration", ""); } + metaData.putValue("entityName", entity.getName()); } else { entityNode = json.createObjectNode(); if (actionType == ActionType.ATTRIBUTES_UPDATED) { 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/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java index 42380dfe93..26c38188fd 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.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -24,16 +25,22 @@ import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; 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..154acf1129 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; @@ -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..39b472c988 --- /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}") + private int notificationSystemExecutorThreadPoolSize; + + @Override + protected int getThreadPollSize() { + return notificationSystemExecutorThreadPoolSize; + } + +} 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..bdbf584a3e 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 @@ -62,6 +62,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 +83,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 +98,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 +173,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Autowired private JwtSettingsService jwtSettingsService; + @Autowired + private NotificationSettingsService notificationSettingsService; + @Bean protected BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -671,4 +676,17 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { } } + @Override + public void createDefaultNotificationConfigs() { + notificationSettingsService.createDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); + PageDataIterable tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500); + for (TenantId tenantId : tenants) { + try { + notificationSettingsService.createDefaultNotificationConfigs(tenantId); + } catch (Exception e) { + log.warn("Failed to create default notification configs for tenant {}: {}", tenantId, e.getMessage()); + } + } + } + } 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/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java new file mode 100644 index 0000000000..0f165fb0d8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -0,0 +1,414 @@ +/** + * 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.NotificationCenter; +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.NotificationProcessingContext; +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.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.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.PushDeliveryMethodNotificationTemplate; +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.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.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.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.concurrent.atomic.AtomicInteger; +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 NotificationExecutorService notificationExecutor; + private final DbCallbackExecutorService dbCallbackExecutorService; + private final NotificationsTopicService notificationsTopicService; + private final TbQueueProducerProvider producerProvider; + private Map channels; + + + @Override + public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) { + 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 = notificationTargetService.findNotificationTargetsByTenantIdAndIds(tenantId, + notificationRequest.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList())); + + notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { + if (!template.isEnabled()) return; + if (deliveryMethod == NotificationDeliveryMethod.SLACK) { + if (!settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod)) { + throw new IllegalArgumentException("Slack must be configured in the settings"); + } + } + 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); + } + + UserId senderId = notificationRequest.getSenderId(); + if (senderId != null) { + if (stats.getErrors().isEmpty()) { + int sent = stats.getSent().values().stream().mapToInt(AtomicInteger::get).sum(); + sendBasicNotification(tenantId, senderId, "Notifications sent", + "All notifications were successfully sent (" + sent + ")"); + } else { + int failures = stats.getErrors().values().stream().mapToInt(Map::size).sum(); + sendBasicNotification(tenantId, senderId, "Notification failure", + "Some notifications were not sent (" + failures + ")"); // TODO: 'Go to' button + } + } + }, dbCallbackExecutorService); + }); + + return savedNotificationRequest; + } + + private List> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { + Iterable recipients; + switch (target.getConfiguration().getType()) { + case PLATFORM_USERS: { + recipients = new PageDataIterable<>(pageLink -> { + return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), ctx.getCustomerId(), target.getConfiguration(), pageLink); + }, 200); + 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, PushDeliveryMethodNotificationTemplate 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() + .notification(notification) + .updateType(ComponentLifecycleEvent.CREATED) + .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() + .notification(notification) + .updateType(ComponentLifecycleEvent.CREATED) + .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() + .notificationId(notificationId) + .updatedStatus(NotificationStatus.READ) + .updateType(ComponentLifecycleEvent.UPDATED) + .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() + .allNotifications(true) + .updatedStatus(NotificationStatus.READ) + .updateType(ComponentLifecycleEvent.UPDATED) + .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() + .notification(notification) + .updateType(ComponentLifecycleEvent.DELETED) + .build(); + onNotificationUpdate(tenantId, recipientId, update); + } + } + + @Override + public NotificationRequest updateNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) { + log.debug("Updating notification request {}", notificationRequest.getId()); + notificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest); + // marking related notifications as unread: TODO: causes each subscription to fetch notifications on each request update + notificationService.updateNotificationsStatusByRequestId(tenantId, notificationRequest.getId(), NotificationStatus.SENT); + + // TODO: no need to send request update for other than PLATFORM_USERS target type + onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder() + .notificationRequestId(notificationRequest.getId()) + .notificationInfo(notificationRequest.getInfo()) + .deleted(false) + .build()); + return notificationRequest; + } + + @Override + public void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId) { + log.debug("Deleting notification request {}", notificationRequestId); + NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId); + notificationRequestService.deleteNotificationRequest(tenantId, notificationRequest); + + // TODO: no need to send request update for other than PLATFORM_USERS target type + if (notificationRequest.isSent()) { + onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder() + .notificationRequestId(notificationRequestId) + .deleted(true) + .build()); + } + 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.PUSH; + } + + @Override + protected String getExecutorPrefix() { + return "notification"; + } + + @Autowired + public void setChannels(List channels, NotificationCenter websocketNotificationChannel) { + this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); + this.channels.put(NotificationDeliveryMethod.PUSH, (NotificationChannel) websocketNotificationChannel); + } + +} 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/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..be80d9d75c --- /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.common.data.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..0357cccf3a --- /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.common.data.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..2c9d948fb8 --- /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.common.data.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..5d97d73a90 --- /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.common.data.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..c17b8f3bcd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java @@ -0,0 +1,237 @@ +/** + * 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 com.google.common.util.concurrent.ListenableFuture; +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.common.util.DonAsynchron; +import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.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.RuleChainId; +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.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.NotificationRuleService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.executors.NotificationExecutorService; +import org.thingsboard.server.service.notification.rule.trigger.AlarmTriggerProcessor.AlarmTriggerObject; +import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; +import org.thingsboard.server.service.notification.rule.trigger.RuleEngineComponentLifecycleEventTriggerProcessor.RuleEngineComponentLifecycleEventTriggerObject; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultNotificationRuleProcessingService implements NotificationRuleProcessingService { + + private final NotificationRuleService notificationRuleService; + private final NotificationRequestService notificationRequestService; + @Autowired @Lazy + private NotificationCenter notificationCenter; + private Map triggerProcessors; + + private final NotificationExecutorService notificationExecutor; + private final DbCallbackExecutorService dbCallbackExecutor; + + private final Map msgTypeToTriggerType = Map.of( + DataConstants.INACTIVITY_EVENT, NotificationRuleTriggerType.DEVICE_INACTIVITY, + DataConstants.ENTITY_CREATED, NotificationRuleTriggerType.ENTITY_ACTION, + DataConstants.ENTITY_UPDATED, NotificationRuleTriggerType.ENTITY_ACTION, + DataConstants.ENTITY_DELETED, NotificationRuleTriggerType.ENTITY_ACTION, + DataConstants.COMMENT_CREATED, NotificationRuleTriggerType.ALARM_COMMENT, + DataConstants.COMMENT_UPDATED, NotificationRuleTriggerType.ALARM_COMMENT + ); + + @Override + public void process(TenantId tenantId, TbMsg ruleEngineMsg) { + String msgType = ruleEngineMsg.getType(); + NotificationRuleTriggerType triggerType = msgTypeToTriggerType.get(msgType); + if (triggerType == null) { + return; + } + + processTrigger(tenantId, triggerType, ruleEngineMsg.getOriginator(), ruleEngineMsg); + } + + @Override + public void process(TenantId tenantId, Alarm alarm, boolean deleted) { + AlarmTriggerObject triggerObject = AlarmTriggerObject.builder() + .alarm(alarm) + .deleted(deleted) + .build(); + processTrigger(tenantId, NotificationRuleTriggerType.ALARM, alarm.getId(), triggerObject); + } + + @Override + public void process(TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, EntityId componentId, String componentName, ComponentLifecycleEvent eventType, Exception error) { + RuleEngineComponentLifecycleEventTriggerObject triggerObject = RuleEngineComponentLifecycleEventTriggerObject.builder() + .ruleChainId(ruleChainId) + .ruleChainName(ruleChainName) + .componentId(componentId) + .componentName(componentName) + .eventType(eventType) + .error(error) + .build(); + processTrigger(tenantId, NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, componentId, triggerObject); + } + + private void processTrigger(TenantId tenantId, NotificationRuleTriggerType triggerType, EntityId originatorEntityId, Object triggerObject) { + ListenableFuture> rulesFuture = dbCallbackExecutor.submit(() -> { + return notificationRuleService.findNotificationRulesByTenantIdAndTriggerType(tenantId, triggerType); + }); + DonAsynchron.withCallback(rulesFuture, rules -> { + for (NotificationRule rule : rules) { + notificationExecutor.submit(() -> { + processNotificationRule(rule, originatorEntityId, triggerObject); + }); + } + }, e -> { + log.error("Failed to find notification rules by trigger type {}", triggerType, e); + }); + } + + private void processNotificationRule(NotificationRule rule, EntityId originatorEntityId, Object triggerObject) { + NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); + log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); + + if (triggerConfig.getTriggerType().isUpdatable()) { + List notificationRequests = notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(rule.getTenantId(), rule.getId(), originatorEntityId); + if (!notificationRequests.isEmpty()) { + if (matchesClearRule(triggerObject, triggerConfig)) { + notificationRequests = notificationRequests.stream() + .filter(notificationRequest -> { + if (!notificationRequest.isSent()) { + dbCallbackExecutor.submit(() -> { + notificationCenter.deleteNotificationRequest(rule.getTenantId(), notificationRequest.getId()); + }); + return false; + } else { + return true; + } + }) + .collect(Collectors.toList()); + // not returning because we need to update notifications if any + } + + NotificationInfo notificationInfo = constructNotificationInfo(triggerObject, triggerConfig); + for (NotificationRequest notificationRequest : notificationRequests) { + NotificationInfo previousNotificationInfo = notificationRequest.getInfo(); + if (!notificationInfo.equals(previousNotificationInfo)) { + notificationRequest.setInfo(notificationInfo); + dbCallbackExecutor.submit(() -> { + notificationCenter.updateNotificationRequest(rule.getTenantId(), notificationRequest); + }); + } + } + return; + } + } + + if (!matchesFilter(triggerObject, triggerConfig)) { + return; + } + + NotificationInfo notificationInfo = constructNotificationInfo(triggerObject, triggerConfig); + rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> { + notificationExecutor.submit(() -> { + try { + log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delay, targets); + submitNotificationRequest(targets, rule, originatorEntityId, notificationInfo, delay); + } catch (Exception e) { + log.error("Failed to submit notification request for rule {}", rule.getId(), e); + } + }); + }); + } + + private boolean matchesFilter(Object triggerObject, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).matchesFilter(triggerObject, triggerConfig); + } + + private boolean matchesClearRule(Object triggerObject, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).matchesClearRule(triggerObject, triggerConfig); + } + + private NotificationInfo constructNotificationInfo(Object triggerObject, NotificationRuleTriggerConfig triggerConfig) { + return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(triggerObject, triggerConfig); + } + + private void submitNotificationRequest(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(rule.getTenantId()) + .targets(targets) + .templateId(rule.getTemplateId()) + .additionalConfig(config) + .info(notificationInfo) + .ruleId(rule.getId()) + .originatorEntityId(originatorEntityId) + .build(); + notificationCenter.processNotificationRequest(rule.getTenantId(), notificationRequest); + } + + @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(); + dbCallbackExecutor.submit(() -> { + List scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId); + for (NotificationRequestId notificationRequestId : scheduledForRule) { + notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId); + } + }); + } + + @Autowired + public void setTriggerProcessors(Collection processors) { + this.triggerProcessors = processors.stream() + .collect(Collectors.toMap(NotificationRuleTriggerProcessor::getTriggerType, p -> p)); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/NotificationRuleProcessingService.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/NotificationRuleProcessingService.java new file mode 100644 index 0000000000..d769983b6a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/NotificationRuleProcessingService.java @@ -0,0 +1,34 @@ +/** + * 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 org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.TbMsg; + +public interface NotificationRuleProcessingService { + + void process(TenantId tenantId, TbMsg ruleEngineMsg); + + void process(TenantId tenantId, Alarm alarm, boolean deleted); + + void process(TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, + EntityId componentId, String componentName, ComponentLifecycleEvent eventType, Exception error); + +} 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..7a1ef3949b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.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.notification.rule.trigger; + +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +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; + +@Service +public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(TbMsg ruleEngineMsg, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { + return ruleEngineMsg.getMetaData().getValue("comment") != null; + } + + @Override + public NotificationInfo constructNotificationInfo(TbMsg ruleEngineMsg, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { + AlarmComment comment = JacksonUtil.fromString(ruleEngineMsg.getMetaData().getValue("comment"), AlarmComment.class); + Alarm alarm = JacksonUtil.fromString(ruleEngineMsg.getData(), Alarm.class); + return AlarmCommentNotificationInfo.builder() + .comment(comment.getComment().get("text").asText()) + .alarmType(alarm.getType()) + .alarmId(comment.getAlarmId().getId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM_COMMENT; + } + +} 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..d5e3660782 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -0,0 +1,80 @@ +/** + * 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.Builder; +import lombok.Data; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.alarm.Alarm; +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.ClearRule; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.service.notification.rule.trigger.AlarmTriggerProcessor.AlarmTriggerObject; + +@Service +public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) { + Alarm alarm = triggerObject.getAlarm(); + return (CollectionUtils.isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && + (CollectionUtils.isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())); + } + + @Override + public boolean matchesClearRule(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) { + if (triggerObject.isDeleted()) { + return true; + } + Alarm alarm = triggerObject.getAlarm(); + ClearRule clearRule = triggerConfig.getClearRule(); + if (clearRule != null) { + if (clearRule.getAlarmStatus() != null) { + return clearRule.getAlarmStatus().equals(alarm.getStatus()); + } + } + return false; + } + + @Override + public NotificationInfo constructNotificationInfo(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) { + Alarm alarm = triggerObject.getAlarm(); + return AlarmNotificationInfo.builder() + .alarmId(alarm.getUuidId()) + .alarmType(alarm.getType()) + .alarmOriginator(alarm.getOriginator()) + .alarmSeverity(alarm.getSeverity()) + .alarmStatus(alarm.getStatus()) + .alarmCustomerId(alarm.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ALARM; + } + + @Data + @Builder + public static class AlarmTriggerObject { + private final Alarm alarm; + private final boolean deleted; + } + +} 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..a193368b12 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java @@ -0,0 +1,65 @@ +/** + * 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.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.service.profile.TbDeviceProfileCache; + +@Service +@RequiredArgsConstructor +public class DeviceInactivityTriggerProcessor implements NotificationRuleTriggerProcessor { + + private final TbDeviceProfileCache deviceProfileCache; + + @Override + public boolean matchesFilter(TbMsg ruleEngineMsg, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) { + DeviceId deviceId = (DeviceId) ruleEngineMsg.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(TbMsg ruleEngineMsg, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) { + return DeviceInactivityNotificationInfo.builder() + .deviceId(ruleEngineMsg.getOriginator().getId()) + .deviceName(ruleEngineMsg.getMetaData().getValue("deviceName")) + .deviceType(ruleEngineMsg.getMetaData().getValue("deviceType")) + .deviceCustomerId(ruleEngineMsg.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.DEVICE_INACTIVITY; + } + +} 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..58b5e12ca9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java @@ -0,0 +1,77 @@ +/** + * 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.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +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 java.util.UUID; + +@Service +public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(TbMsg ruleEngineMsg, EntityActionNotificationRuleTriggerConfig triggerConfig) { + String msgType = ruleEngineMsg.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 || ruleEngineMsg.getOriginator().getEntityType() == triggerConfig.getEntityType(); + } + + @Override + public NotificationInfo constructNotificationInfo(TbMsg ruleEngineMsg, EntityActionNotificationRuleTriggerConfig triggerConfig) { + EntityId entityId = ruleEngineMsg.getOriginator(); + String msgType = ruleEngineMsg.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() + .entityType(entityId.getEntityType()) + .entityId(entityId.getId()) + .entityName(ruleEngineMsg.getMetaData().getValue("entityName")) + .actionType(actionType) + .originatorUserId(UUID.fromString(ruleEngineMsg.getMetaData().getValue("userId"))) + .originatorUserName(ruleEngineMsg.getMetaData().getValue("userName")) + .entityCustomerId(ruleEngineMsg.getCustomerId()) + .build(); + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.ENTITY_ACTION; + } + +} 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..4980906f7a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/NotificationRuleTriggerProcessor.java @@ -0,0 +1,34 @@ +/** + * 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.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; + +public interface NotificationRuleTriggerProcessor { + + boolean matchesFilter(T triggerObject, C triggerConfig); + + default boolean matchesClearRule(T triggerObject, C triggerConfig) { + return false; + } + + NotificationInfo constructNotificationInfo(T triggerObject, 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..53b53ab0aa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java @@ -0,0 +1,110 @@ +/** + * 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 com.google.common.base.Strings; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +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.service.notification.rule.trigger.RuleEngineComponentLifecycleEventTriggerProcessor.RuleEngineComponentLifecycleEventTriggerObject; + +import java.util.Set; + +@Service +public class RuleEngineComponentLifecycleEventTriggerProcessor implements NotificationRuleTriggerProcessor { + + @Override + public boolean matchesFilter(RuleEngineComponentLifecycleEventTriggerObject triggerObject, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) { + if (CollectionUtils.isNotEmpty(triggerConfig.getRuleChains())) { + if (!triggerConfig.getRuleChains().contains(triggerObject.getRuleChainId().getId())) { + return false; + } + } + + EntityType componentType = triggerObject.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(triggerObject.getEventType())) { + return false; + } + if (onlyFailures) { + return triggerObject.getError() != null; + } + return true; + } + + @Override + public NotificationInfo constructNotificationInfo(RuleEngineComponentLifecycleEventTriggerObject triggerObject, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) { + return RuleEngineComponentLifecycleEventNotificationInfo.builder() + .ruleChainId(triggerObject.getRuleChainId()) + .ruleChainName(triggerObject.getRuleChainName()) + .componentId(triggerObject.getComponentId()) + .componentName(triggerObject.getComponentName()) + .eventType(triggerObject.getEventType()) + .error(getErrorMsg(triggerObject.getError())) + .build(); + } + + private String getErrorMsg(Exception error) { + String errorMsg = error != null ? error.getMessage() : null; + errorMsg = Strings.nullToEmpty(errorMsg); + int lengthLimit = 150; + if (errorMsg.length() > lengthLimit) { + errorMsg = StringUtils.substring(errorMsg, 0, lengthLimit + 1).trim() + "[...]"; + } + return errorMsg; + } + + @Override + public NotificationRuleTriggerType getTriggerType() { + return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; + } + + @Data + @Builder + public static class RuleEngineComponentLifecycleEventTriggerObject { + private final RuleChainId ruleChainId; + private final String ruleChainName; + private final EntityId componentId; + private final String componentName; + private final ComponentLifecycleEvent eventType; + private final Exception error; + } + +} 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..baded64603 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,12 @@ 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.notification.rule.NotificationRuleProcessingService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; @@ -74,12 +77,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 +129,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer; private final TbQueueConsumer> firmwareStatesConsumer; @@ -147,8 +155,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService jwtSettingsService) { - super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); + ApplicationEventPublisher eventPublisher, + NotificationRuleProcessingService notificationRuleProcessingService, + Optional jwtSettingsService, + NotificationSchedulerService notificationSchedulerService) { + super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, notificationRuleProcessingService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService); this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer(); this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer(); this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer(); @@ -161,6 +172,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer; protected final Optional jwtSettingsService; @@ -83,7 +87,9 @@ public abstract class AbstractConsumerService> nfConsumer, Optional jwtSettingsService) { + PartitionService partitionService, ApplicationEventPublisher eventPublisher, + NotificationRuleProcessingService notificationRuleProcessingService, + TbQueueConsumer> nfConsumer, Optional jwtSettingsService) { this.actorContext = actorContext; this.encodingService = encodingService; this.tenantProfileCache = tenantProfileCache; @@ -91,6 +97,8 @@ 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..7bd5fb7817 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java @@ -0,0 +1,154 @@ +/** + * 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.server.common.data.notification.targets.slack.SlackConversation; +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.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"; + } + if (error.contains("missing_scope")) { + String neededScope = response.getNeeded(); + throw new RuntimeException("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..80fd643253 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; @@ -149,7 +150,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 NotificationRuleProcessingService notificationRuleProcessingService; private final Map> subscriptionsByEntityId = new ConcurrentHashMap<>(); private final Map> subscriptionsByWsSessionId = new ConcurrentHashMap<>(); @@ -306,6 +296,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene s -> alarm.getCreatedTime() >= s.getTs() || alarm.getAssignTs() >= s.getTs(), alarm, false ); + notificationRuleProcessingService.process(tenantId, alarm, false); callback.onSuccess(); } @@ -322,6 +313,52 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene s -> alarm.getCreatedTime() >= s.getTs(), alarm, true ); + notificationRuleProcessingService.process(tenantId, alarm, true); + 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(); } 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..77d8c916e1 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,7 +26,7 @@ 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; 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/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/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..f412e76ad9 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,32 @@ 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.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.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; - } - @Override - String getExecutorPrefix() { + protected String getExecutorPrefix() { return "alarm"; } @@ -144,7 +121,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 +209,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,17 +226,11 @@ 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); + }); } }); } @@ -271,17 +240,11 @@ 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); + }); } }); } @@ -315,7 +278,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/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..f4dc689f35 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,37 @@ 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("Failed to handle WS cmd: {}", cmd, e); + } + } + } + } + } + + private void handleWsEntityDataCmd(WebSocketSessionRef sessionRef, EntityDataCmd cmd) { String sessionId = sessionRef.getSessionId(); log.debug("[{}] Processing: {}", sessionId, cmd); @@ -259,7 +272,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 +282,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 +292,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 +330,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 +364,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 +436,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 +461,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 +485,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 +527,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 +577,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 +598,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 +635,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 +659,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 +683,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 +698,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 +736,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 +751,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 +786,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 +794,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 +809,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 +823,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 +838,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 +848,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 +865,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 +1017,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..a3d210ccda --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -0,0 +1,256 @@ +/** + * 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(); + switch (update.getUpdateType()) { + case CREATED: { + 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)); + break; + } + case UPDATED: { + if (update.getUpdatedStatus() == 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)); + } + } + break; + } + case DELETED: { + 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()); + } + break; + } + } + } + + private void handleNotificationRequestUpdate(NotificationsSubscription subscription, NotificationRequestUpdate update) { + log.trace("[{}, subId: {}] Handling notification request update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + fetchUnreadNotifications(subscription); // FIXME: figure out how not to fetch notifications on each request update... + 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); + Notification notification = update.getNotification(); + switch (update.getUpdateType()) { + case CREATED: { + subscription.getUnreadCounter().incrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + break; + } + case UPDATED: { + if (update.getUpdatedStatus() == NotificationStatus.READ) { + if (update.isAllNotifications()) { + fetchUnreadNotificationsCount(subscription); + } else { + subscription.getUnreadCounter().decrementAndGet(); + } + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + break; + } + case DELETED: { + if (notification.getStatus() != NotificationStatus.READ) { + subscription.getUnreadCounter().decrementAndGet(); + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + break; + } + } + } + + private void handleNotificationRequestUpdate(NotificationsCountSubscription subscription, NotificationRequestUpdate update) { + log.trace("[{}, subId: {}] Handling notification request update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); + fetchUnreadNotificationsCount(subscription); // FIXME: figure out how not to fetch notifications on each request update... + sendUpdate(subscription.getSessionId(), subscription.createUpdate()); + } + + + private void sendUpdate(String sessionId, CmdUpdate update) { + log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update); + wsService.sendWsMsg(sessionId, update); + } + + + @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()); + } + +} 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..e3563a32f2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationRequestUpdate.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.sub; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.id.NotificationRequestId; +import org.thingsboard.server.common.data.notification.info.NotificationInfo; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationRequestUpdate { + private NotificationRequestId notificationRequestId; + private NotificationInfo notificationInfo; + 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..be9bd8d5ae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.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.ws.notification.sub; + +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 org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; + +import java.util.UUID; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class NotificationUpdate { + + private NotificationId notificationId; + private Notification notification; + + boolean allNotifications; + + private NotificationStatus updatedStatus; + private ComponentLifecycleEvent updateType; + + 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..08fd0eef1e 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}" + notificationRequests: + timeToLiveInMinutes: "${CACHE_SPECS_NOTIFICATION_RULES_TTL:1440}" + maxSize: "${CACHE_SPECS_NOTIFICATION_RULES_MAX_SIZE:10000}" 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:30}" + 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 c09f259aaa..48db5912a0 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(); @@ -357,21 +362,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { 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); @@ -542,6 +550,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 +663,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 +789,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/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/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java new file mode 100644 index 0000000000..52d115a7dc --- /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.PushDeliveryMethodNotificationTemplate; +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.PUSH}; + } + 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.setDefaultTextTemplate(text); + config.setDeliveryMethodsTemplates(new HashMap<>()); + for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { + DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; + switch (deliveryMethod) { + case PUSH: { + PushDeliveryMethodNotificationTemplate template = new PushDeliveryMethodNotificationTemplate(); + 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); + 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..bb2bbce14a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -0,0 +1,619 @@ +/** + * 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.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.PushDeliveryMethodNotificationTemplate; +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.PUSH); + } + 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 whenNotificationRequestIsUpdated_thenUpdateNotifications() throws Exception { + wsClient.subscribeForUnreadNotifications(10); + wsClient.waitForReply(true); + + NotificationTarget notificationTarget = createNotificationTarget(customerUserId); + String notificationText = "Text"; + wsClient.registerWaitForUpdate(); + NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText); + wsClient.waitForUpdate(true); + Notification initialNotification = wsClient.getLastDataUpdate().getUpdate(); + loginCustomerUser(); + assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(initialNotification); + assertThat(initialNotification.getInfo()).isNotNull().isEqualTo(notificationRequest.getInfo()); + + wsClient.registerWaitForUpdate(); + UserOriginatedNotificationInfo newNotificationInfo = new UserOriginatedNotificationInfo(); + newNotificationInfo.setDescription("New description"); + notificationRequest.setInfo(newNotificationInfo); + notificationCenter.updateNotificationRequest(tenantId, notificationRequest); + wsClient.waitForUpdate(true); + Notification updatedNotification = wsClient.getLastDataUpdate().getNotifications().iterator().next(); + assertThat(updatedNotification.getInfo()).isEqualTo(newNotificationInfo); + assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(updatedNotification); + } + + @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.PUSH); + 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.PUSH)).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); + + 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"); + } + 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(); + templateConfig.setDefaultTextTemplate("Default message for SMS and PUSH: ${recipientEmail}"); + templateConfig.setNotificationSubject("Default subject for EMAIL: ${recipientEmail}"); + HashMap templates = new HashMap<>(); + templateConfig.setDeliveryMethodsTemplates(templates); + notificationTemplate.setConfiguration(templateConfig); + + PushDeliveryMethodNotificationTemplate pushNotificationTemplate = new PushDeliveryMethodNotificationTemplate(); + pushNotificationTemplate.setEnabled(true); + // using default message for push + pushNotificationTemplate.setSubject("Subject for PUSH: ${recipientEmail}"); + templates.put(NotificationDeliveryMethod.PUSH, pushNotificationTemplate); + + SmsDeliveryMethodNotificationTemplate smsNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); + smsNotificationTemplate.setEnabled(true); + // using default message for sms + templates.put(NotificationDeliveryMethod.SMS, smsNotificationTemplate); + + EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailNotificationTemplate.setEnabled(true); + emailNotificationTemplate.setBody("Message for EMAIL: ${recipientEmail}"); + // using default subject for email + 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); + + Map processedTemplates = preview.getProcessedTemplates(); + assertThat(processedTemplates.get(NotificationDeliveryMethod.PUSH)).asInstanceOf(type(PushDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .startsWith("Default message for SMS and PUSH") + .endsWith(requestorEmail); + assertThat(template.getSubject()) + .startsWith("Subject for PUSH") + .endsWith(requestorEmail); + }); + assertThat(processedTemplates.get(NotificationDeliveryMethod.SMS)).asInstanceOf(type(SmsDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .startsWith("Default message for SMS and PUSH") + .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("Default subject for EMAIL") + .endsWith(requestorEmail); + }); + assertThat(processedTemplates.get(NotificationDeliveryMethod.SLACK)).asInstanceOf(type(SlackDeliveryMethodNotificationTemplate.class)) + .satisfies(template -> { + assertThat(template.getBody()) + .isEqualTo("Message for SLACK: ${recipientEmail}"); // ${recipientEmail} should not be processed + }); + } + + @Test + public void testNotificationRequestInfo() throws Exception { + NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ + NotificationDeliveryMethod.PUSH, NotificationDeliveryMethod.EMAIL + }; + 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.PUSH, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS); + wsClient.waitForUpdate(); + + await().atMost(2, TimeUnit.SECONDS) + .until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); + NotificationRequestStats stats = getStats(notificationRequest.getId()); + + assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(1); + assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(1); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.SMS)).size().isOne(); + } + + @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.PUSH); + }); + 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(); + config.setDefaultTextTemplate("To Slack :) ${recipientEmail}"); + SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate(); + slackNotificationTemplate.setEnabled(true); + 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(config.getDefaultTextTemplate())); + 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..60915a2952 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -0,0 +1,469 @@ +/** + * 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.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.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.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.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 +public class NotificationRuleApiTest extends AbstractNotificationApiTest { + + @SpyBean + private AlarmSubscriptionService alarmSubscriptionService; + @Autowired + private NotificationRequestService notificationRequestService; + + @SpyBean + private AlarmService alarmService; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + } + + @Test + public void testNotificationRuleProcessing_entityActionTrigger() throws Exception { + String notificationSubject = "${actionType}: ${entityType} [${entityId}]"; + String notificationText = "User: ${originatorUserName}"; + NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.GENERAL, notificationSubject, notificationText, NotificationDeliveryMethod.PUSH); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Push-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.PUSH); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Push-notification on any alarm"); + notificationRule.setTemplateId(notificationTemplate.getId()); + notificationRule.setTriggerType(NotificationRuleTriggerType.ALARM); + + AlarmNotificationRuleTriggerConfig triggerConfig = new AlarmNotificationRuleTriggerConfig(); + triggerConfig.setAlarmTypes(null); + triggerConfig.setAlarmSeverities(null); + 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)); + + AlarmStatus expectedStatus = AlarmStatus.ACTIVE_UNACK; + AlarmSeverity expectedSeverity = AlarmSeverity.CRITICAL; + + assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + + "severity: " + expectedSeverity + ", deviceId: " + device.getId()); + assertThat(notification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity); + + 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(expectedSeverity); + assertThat(info.getAlarmStatus()).isEqualTo(expectedStatus); + }); + + clients.values().forEach(wsClient -> wsClient.registerWaitForUpdate()); + alarmSubscriptionService.ackAlarm(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().getNotifications().stream().findFirst().get(); + assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + + "severity: " + expectedSeverity + ", deviceId: " + device.getId()); + assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity); + + wsClient.close(); + }); + + // TODO: test severity changes + } + + @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.PUSH); + + NotificationRule notificationRule = new NotificationRule(); + notificationRule.setName("Push-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); + + AlarmNotificationRuleTriggerConfig.ClearRule clearRule = new AlarmNotificationRuleTriggerConfig.ClearRule(); + clearRule.setAlarmStatus(AlarmStatus.CLEARED_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(), null, System.currentTimeMillis()); + getWsClient().waitForUpdate(true); + notification = getWsClient().getLastDataUpdate().getNotifications().iterator().next(); + assertThat(notification.getSubject()).isEqualTo("CRITICAL alarm '" + alarmType + "' is CLEARED_UNACK"); + + assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); + } + + @Test + public void testNotificationRuleProcessing_ruleEngineComponentLifecycleEvent_ruleNodeStartError() { + String subject = "Rule Node '${componentName}' in Rule Chain '${ruleChainName}' failed to start"; + String text = "The error: ${error}"; + NotificationTemplate template = createNotificationTemplate(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, subject, text, NotificationDeliveryMethod.PUSH); + + NotificationRule rule = new NotificationRule(); + rule.setName("Rule node start-up failures in my rule chain"); + rule.setTemplateId(template.getId()); + rule.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + + RuleChain ruleChain = createEmptyRuleChain("My Rule Chain"); + var triggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig(); + triggerConfig.setRuleChains(Set.of(ruleChain.getUuidId())); + triggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED)); + triggerConfig.setOnlyRuleChainLifecycleFailures(true); + + triggerConfig.setTrackRuleNodeEvents(true); + triggerConfig.setRuleNodeEvents(Set.of(ComponentLifecycleEvent.STARTED)); + triggerConfig.setOnlyRuleNodeLifecycleFailures(true); + rule.setTriggerConfig(triggerConfig); + + NotificationTarget target = createNotificationTarget(tenantAdminUserId); + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + recipientsConfig.setTargets(List.of(target.getUuidId())); + rule.setRecipientsConfig(recipientsConfig); + rule = saveNotificationRule(rule); + + getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + getWsClient().registerWaitForUpdate(); + + addRuleNodeWithError(ruleChain.getId(), "My generator"); + + getWsClient().waitForUpdate(10000, true); + Notification notification = getWsClient().getLastDataUpdate().getUpdate(); + + assertThat(notification.getType()).isEqualTo(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT); + assertThat(notification.getSubject()).isEqualTo("Rule Node 'My generator' in Rule Chain 'My Rule Chain' failed to start"); + assertThat(notification.getText()).startsWith("The error: Can't compile script"); + } + + @Test + public void testNotificationRuleInfo() throws Exception { + NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.PUSH, 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..17e8ca1043 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java @@ -0,0 +1,124 @@ +/** + * 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.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.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 { + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + } + + @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); + config.setDefaultTextTemplate("Default text"); + config.setNotificationSubject(null); + EmailDeliveryMethodNotificationTemplate emailTemplate = new EmailDeliveryMethodNotificationTemplate(); + emailTemplate.setEnabled(true); + emailTemplate.setBody(null); + emailTemplate.setSubject(null); + config.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.EMAIL, emailTemplate + )); + notificationTemplate.setName("