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 extends NotificationRecipient> 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 extends TbSubscription, T> updateProcessor;
@Override
public boolean equals(Object o) {
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
index 2149814436..cd560bbbaf 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
@@ -16,5 +16,5 @@
package org.thingsboard.server.service.subscription;
public enum TbSubscriptionType {
- TIMESERIES, ATTRIBUTES, ALARMS
+ TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS, NOTIFICATIONS_COUNT
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
index c30ac9f0d5..914c8a7c55 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
@@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@@ -51,10 +52,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesDeletePr
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto;
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
-import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
-import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
-import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
+import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
+import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
+import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription;
+import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription;
+import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
+import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
+import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.ArrayList;
import java.util.HashMap;
@@ -107,6 +113,17 @@ public class TbSubscriptionUtils {
.setTs(alarmSub.getTs());
msgBuilder.setAlarmSub(alarmSubProto.build());
break;
+ case NOTIFICATIONS:
+ NotificationsSubscription notificationsSub = (NotificationsSubscription) subscription;
+ msgBuilder.setNotificationsSub(TransportProtos.NotificationsSubscriptionProto.newBuilder()
+ .setSub(subscriptionProto)
+ .setLimit(notificationsSub.getLimit()));
+ break;
+ case NOTIFICATIONS_COUNT:
+ NotificationsCountSubscription notificationsCountSub = (NotificationsCountSubscription) subscription;
+ msgBuilder.setNotificationsCountSub(TransportProtos.NotificationsCountSubscriptionProto.newBuilder()
+ .setSub(subscriptionProto));
+ break;
}
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
}
@@ -168,6 +185,29 @@ public class TbSubscriptionUtils {
return builder.build();
}
+ public static NotificationsSubscription fromProto(TransportProtos.NotificationsSubscriptionProto notificationsSub) {
+ TbSubscriptionProto sub = notificationsSub.getSub();
+ return NotificationsSubscription.builder()
+ .serviceId(sub.getServiceId())
+ .sessionId(sub.getSessionId())
+ .subscriptionId(sub.getSubscriptionId())
+ .tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB())))
+ .entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB())))
+ .limit(notificationsSub.getLimit())
+ .build();
+ }
+
+ public static NotificationsCountSubscription fromProto(TransportProtos.NotificationsCountSubscriptionProto notificationsCountSub) {
+ TbSubscriptionProto sub = notificationsCountSub.getSub();
+ return NotificationsCountSubscription.builder()
+ .serviceId(sub.getServiceId())
+ .sessionId(sub.getSessionId())
+ .subscriptionId(sub.getSubscriptionId())
+ .tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB())))
+ .entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB())))
+ .build();
+ }
+
public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) {
if (proto.getErrorCode() > 0) {
return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
@@ -343,4 +383,50 @@ public class TbSubscriptionUtils {
msgBuilder.setAlarmDelete(builder);
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
}
+
+ public static ToCoreNotificationMsg notificationsSubUpdateToProto(TbSubscription subscription, NotificationsSubscriptionUpdate update) {
+ TransportProtos.NotificationsSubscriptionUpdateProto.Builder updateProto = TransportProtos.NotificationsSubscriptionUpdateProto.newBuilder()
+ .setSessionId(subscription.getSessionId())
+ .setSubscriptionId(subscription.getSubscriptionId());
+ if (update.getNotificationUpdate() != null) {
+ updateProto.setNotificationUpdate(JacksonUtil.toString(update.getNotificationUpdate()));
+ }
+ if (update.getNotificationRequestUpdate() != null) {
+ updateProto.setNotificationRequestUpdate(JacksonUtil.toString(update.getNotificationRequestUpdate()));
+ }
+ return ToCoreNotificationMsg.newBuilder()
+ .setToLocalSubscriptionServiceMsg(TransportProtos.LocalSubscriptionServiceMsgProto.newBuilder()
+ .setNotificationsSubUpdate(updateProto)
+ .build())
+ .build();
+ }
+
+ public static ToCoreMsg notificationUpdateToProto(TenantId tenantId, UserId recipientId, NotificationUpdate notificationUpdate) {
+ TransportProtos.NotificationUpdateProto updateProto = TransportProtos.NotificationUpdateProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setRecipientIdMSB(recipientId.getId().getMostSignificantBits())
+ .setRecipientIdLSB(recipientId.getId().getLeastSignificantBits())
+ .setUpdate(JacksonUtil.toString(notificationUpdate))
+ .build();
+ return ToCoreMsg.newBuilder()
+ .setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder()
+ .setNotificationUpdate(updateProto)
+ .build())
+ .build();
+ }
+
+ public static ToCoreNotificationMsg notificationRequestUpdateToProto(TenantId tenantId, NotificationRequestUpdate notificationRequestUpdate) {
+ TransportProtos.NotificationRequestUpdateProto updateProto = TransportProtos.NotificationRequestUpdateProto.newBuilder()
+ .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
+ .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
+ .setUpdate(JacksonUtil.toString(notificationRequestUpdate))
+ .build();
+ return ToCoreNotificationMsg.newBuilder()
+ .setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder()
+ .setNotificationRequestUpdate(updateProto)
+ .build())
+ .build();
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
index a48d3985ee..400cf07e7e 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbTimeseriesSubscription.java
@@ -19,7 +19,7 @@ import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
+import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
import java.util.Map;
import java.util.function.BiConsumer;
@@ -39,9 +39,9 @@ public class TbTimeseriesSubscription extends TbSubscription updateConsumer,
+ BiConsumer, TelemetrySubscriptionUpdate> updateProcessor,
boolean allKeys, Map keyStates, long startTime, long endTime, boolean latestValues) {
- super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer);
+ super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateProcessor);
this.allKeys = allKeys;
this.keyStates = keyStates;
this.startTime = startTime;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
index 85c8ee0f8e..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("");
+
+ validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
+ assertThat(validationError)
+ .contains("notificationSubject must be")
+ .contains("name is malformed");
+
+ config.setDefaultTextTemplate(null);
+
+ validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
+ assertThat(validationError)
+ .contains("defaultTextTemplate").contains("must be specified");
+ }
+
+ @Test
+ public void testTemplatesSearch() throws Exception {
+ NotificationTemplate alarmNotificationTemplate = createNotificationTemplate(NotificationType.ALARM, "Alarm", "Alarm", NotificationDeliveryMethod.PUSH);
+ NotificationTemplate generalNotificationTemplate = createNotificationTemplate(NotificationType.GENERAL, "General", "General", NotificationDeliveryMethod.PUSH);
+ NotificationTemplate entityActionNotificationTemplate = createNotificationTemplate(NotificationType.ENTITY_ACTION, "Entity action", "Entity action", NotificationDeliveryMethod.PUSH);
+
+ assertThat(findTemplates(NotificationType.ALARM)).extracting(IdBased::getId)
+ .containsOnly(alarmNotificationTemplate.getId());
+ assertThat(findTemplates(NotificationType.ENTITY_ACTION)).extracting(IdBased::getId)
+ .containsOnly(entityActionNotificationTemplate.getId());
+ assertThat(findTemplates(NotificationType.GENERAL)).extracting(IdBased::getId)
+ .containsOnly(generalNotificationTemplate.getId());
+
+ assertThat(findTemplates(NotificationType.GENERAL, NotificationType.ALARM)).extracting(IdBased::getId)
+ .containsOnly(generalNotificationTemplate.getId(), alarmNotificationTemplate.getId());
+ assertThat(findTemplates(NotificationType.GENERAL, NotificationType.ENTITY_ACTION)).extracting(IdBased::getId)
+ .containsOnly(generalNotificationTemplate.getId(), entityActionNotificationTemplate.getId());
+
+ assertThat(findTemplates()).extracting(IdBased::getId)
+ .containsOnly(generalNotificationTemplate.getId(), alarmNotificationTemplate.getId(), entityActionNotificationTemplate.getId());
+ }
+
+ private String saveAndGetError(NotificationTemplate notificationTemplate, ResultMatcher statusMatcher) throws Exception {
+ return getErrorMessage(save(notificationTemplate, statusMatcher));
+ }
+
+ private ResultActions save(NotificationTemplate notificationTemplate, ResultMatcher statusMatcher) throws Exception {
+ return doPost("/api/notification/template", notificationTemplate)
+ .andExpect(statusMatcher);
+ }
+
+ private List findTemplates(NotificationType... notificationTypes) throws Exception {
+ PageLink pageLink = new PageLink(100, 0);
+ return doGetTypedWithPageLink("/api/notification/templates?notificationTypes=" + StringUtils.join(notificationTypes, ",") + "&",
+ new TypeReference>() {}, pageLink).getData();
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
index dbbb561b14..be8d2ba379 100644
--- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
@@ -60,7 +60,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
-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/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
index ef00a3050e..a4781bfcd6 100644
--- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
@@ -63,14 +63,14 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.service.DaoSqlTest;
-import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
-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.ws.telemetry.cmd.TelemetryPluginCmdsWrapper;
+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.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
-import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
+import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.io.IOException;
import java.net.ServerSocket;
@@ -108,7 +108,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil
public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest {
@SpyBean
- DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
+ LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
@Autowired
private LwM2mClientContext clientContextTest;
diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
index 7cb5f88733..449ac4c2ae 100644
--- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
+++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
@@ -45,7 +45,7 @@ import org.junit.Assert;
import org.mockito.Mockito;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
-import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
+import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.io.IOException;
@@ -109,12 +109,12 @@ public class LwM2MTestClient {
private LwM2MLocationParams locationParams;
private LwM2mTemperatureSensor lwM2MTemperatureSensor;
private Set clientStates;
- private DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
+ private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext;
public void init(Security security, Configuration coapConfig, int port, boolean isRpc, boolean isBootstrap,
int shortServerId, int shortServerIdBs, Security securityBs,
- DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
+ LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
index f8165b0237..b2fed98f2b 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
@@ -42,7 +42,7 @@ import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.gen.transport.TransportApiProtos;
import org.thingsboard.server.gen.transport.TransportProtos;
-import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
+import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml
index 23e6d8c2f6..953b7094a4 100644
--- a/application/src/test/resources/logback-test.xml
+++ b/application/src/test/resources/logback-test.xml
@@ -21,6 +21,13 @@
+
+
+
+
+
+
+
diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto
index f4992cbba7..c1e6f4d1eb 100644
--- a/common/cluster-api/src/main/proto/queue.proto
+++ b/common/cluster-api/src/main/proto/queue.proto
@@ -548,6 +548,15 @@ message TbAlarmSubscriptionProto {
int64 ts = 2;
}
+message NotificationsSubscriptionProto {
+ TbSubscriptionProto sub = 1;
+ int32 limit = 2;
+}
+
+message NotificationsCountSubscriptionProto {
+ TbSubscriptionProto sub = 1;
+}
+
message TbSubscriptionUpdateProto {
string sessionId = 1;
int32 subscriptionId = 2;
@@ -565,6 +574,27 @@ message TbAlarmSubscriptionUpdateProto {
bool deleted = 6;
}
+message NotificationsSubscriptionUpdateProto {
+ string sessionId = 1;
+ int32 subscriptionId = 2;
+ string notificationUpdate = 3;
+ string notificationRequestUpdate = 4;
+}
+
+message NotificationUpdateProto {
+ int64 tenantIdMSB = 1;
+ int64 tenantIdLSB = 2;
+ int64 recipientIdMSB = 3;
+ int64 recipientIdLSB = 4;
+ string update = 5;
+}
+
+message NotificationRequestUpdateProto {
+ int64 tenantIdMSB = 1;
+ int64 tenantIdLSB = 2;
+ string update = 6;
+}
+
message TbAttributeUpdateProto {
string entityType = 1;
int64 entityIdMSB = 2;
@@ -667,11 +697,16 @@ message SubscriptionMgrMsgProto {
TbAlarmUpdateProto alarmUpdate = 8;
TbAlarmDeleteProto alarmDelete = 9;
TbTimeSeriesDeleteProto tsDelete = 10;
+ NotificationsSubscriptionProto notificationsSub = 11;
+ NotificationsCountSubscriptionProto notificationsCountSub = 12;
+ NotificationUpdateProto notificationUpdate = 13;
+ NotificationRequestUpdateProto notificationRequestUpdate = 14;
}
message LocalSubscriptionServiceMsgProto {
TbSubscriptionUpdateProto subUpdate = 1;
TbAlarmSubscriptionUpdateProto alarmSubUpdate = 2;
+ NotificationsSubscriptionUpdateProto notificationsSubUpdate = 3;
}
message FromDeviceRPCResponseProto {
@@ -923,6 +958,7 @@ message ToCoreMsg {
bytes toDeviceActorNotificationMsg = 4;
EdgeNotificationMsgProto edgeNotificationMsg = 5;
DeviceActivityProto deviceActivityMsg = 6;
+ NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7;
}
/* High priority messages with low latency are handled by ThingsBoard Core Service separately */
@@ -936,6 +972,7 @@ message ToCoreNotificationMsg {
VersionControlResponseMsg vcResponseMsg = 7;
bytes toEdgeSyncRequestMsg = 8;
bytes fromEdgeSyncResponseMsg = 9;
+ SubscriptionMgrMsgProto toSubscriptionMgrMsg = 10;
}
/* Messages that are handled by ThingsBoard RuleEngine Service */
@@ -1000,4 +1037,10 @@ message ToOtaPackageStateServiceMsg {
string type = 8;
}
-
+message NotificationSchedulerServiceMsg {
+ int64 tenantIdMSB = 1;
+ int64 tenantIdLSB = 2;
+ int64 requestIdMSB = 3;
+ int64 requestIdLSB = 4;
+ int64 ts = 5;
+}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestService.java
new file mode 100644
index 0000000000..c781f450cb
--- /dev/null
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRequestService.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2023 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.notification;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.NotificationRequestId;
+import org.thingsboard.server.common.data.id.NotificationRuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.NotificationRequest;
+import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
+import org.thingsboard.server.common.data.notification.NotificationRequestStats;
+import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+
+import java.util.List;
+
+public interface NotificationRequestService {
+
+ NotificationRequest saveNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest);
+
+ NotificationRequest findNotificationRequestById(TenantId tenantId, NotificationRequestId id);
+
+ NotificationRequestInfo findNotificationRequestInfoById(TenantId tenantId, NotificationRequestId id);
+
+ PageData findNotificationRequestsByTenantIdAndOriginatorType(TenantId tenantId, EntityType originatorType, PageLink pageLink);
+
+ PageData findNotificationRequestsInfosByTenantIdAndOriginatorType(TenantId tenantId, EntityType originatorType, PageLink pageLink);
+
+ List findNotificationRequestsIdsByStatusAndRuleId(TenantId tenantId, NotificationRequestStatus requestStatus, NotificationRuleId ruleId);
+
+ List findNotificationRequestsByRuleIdAndOriginatorEntityId(TenantId tenantId, NotificationRuleId ruleId, EntityId originatorEntityId);
+
+ void deleteNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest);
+
+ PageData findScheduledNotificationRequests(PageLink pageLink);
+
+ void updateNotificationRequest(TenantId tenantId, NotificationRequestId requestId, NotificationRequestStatus requestStatus, NotificationRequestStats stats);
+
+ void deleteNotificationRequestsByTenantId(TenantId tenantId);
+
+}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.java
new file mode 100644
index 0000000000..29bdf09a56
--- /dev/null
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationRuleService.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.dao.notification;
+
+import org.thingsboard.server.common.data.id.NotificationRuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.notification.rule.NotificationRule;
+import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
+import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+
+import java.util.List;
+
+public interface NotificationRuleService {
+
+ NotificationRule saveNotificationRule(TenantId tenantId, NotificationRule notificationRule);
+
+ NotificationRule findNotificationRuleById(TenantId tenantId, NotificationRuleId id);
+
+ NotificationRuleInfo findNotificationRuleInfoById(TenantId tenantId, NotificationRuleId id);
+
+ PageData findNotificationRulesInfosByTenantId(TenantId tenantId, PageLink pageLink);
+
+ PageData