Browse Source

Improvements and refactoring

pull/7925/head
ViacheslavKlimov 4 years ago
parent
commit
654e61683a
  1. 12
      application/src/main/data/upgrade/3.4.3/schema_update.sql
  2. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 24
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  5. 1
      application/src/main/java/org/thingsboard/server/controller/NotificationRuleController.java
  6. 1
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  7. 1
      application/src/main/java/org/thingsboard/server/controller/NotificationTemplateController.java
  8. 120
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  9. 43
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationRuleProcessingService.java
  10. 17
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationSchedulerService.java
  11. 39
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  12. 11
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  13. 4
      application/src/main/java/org/thingsboard/server/service/notification/channels/SlackNotificationChannel.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
  15. 85
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  16. 13
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java
  17. 21
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  18. 178
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  19. 425
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  20. 198
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  21. 16
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  22. 2
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java
  23. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java
  24. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  25. 14
      common/data/src/main/java/org/thingsboard/server/common/data/notification/AlarmOriginatedNotificationInfo.java
  26. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java
  27. 18
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationInfo.java
  28. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationOriginatorType.java
  29. 17
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java
  30. 10
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStatus.java
  32. 24
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  33. 23
      common/data/src/main/java/org/thingsboard/server/common/data/notification/RuleNodeOriginatedNotificationInfo.java
  34. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationEscalation.java
  35. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
  36. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
  37. 15
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/AccountNotificationSettings.java
  38. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java
  39. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java
  40. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java
  41. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfigType.java
  42. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java
  43. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java
  44. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java
  45. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackConversation.java
  46. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java
  47. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  48. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java
  49. 11
      dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java
  50. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java
  51. 7
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  52. 12
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  53. 6
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  54. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  55. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  56. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  57. 12
      dao/src/main/resources/sql/schema-entities.sql
  58. 7
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
  59. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  60. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

12
application/src/main/data/upgrade/3.4.3/schema_update.sql

@ -28,7 +28,8 @@ CREATE TABLE IF NOT EXISTS notification_template (
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE, tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
notification_type VARCHAR(255) NOT NULL, notification_type VARCHAR(32) NOT NULL,
notification_subject VARCHAR(255),
configuration VARCHAR(10000) NOT NULL configuration VARCHAR(10000) NOT NULL
); );
@ -46,7 +47,7 @@ CREATE TABLE IF NOT EXISTS notification_request (
id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY, id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_request_tenant_id REFERENCES tenant(id) ON DELETE CASCADE, tenant_id UUID NULL CONSTRAINT fk_notification_request_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
targets VARCHAR(255) NOT NULL, targets VARCHAR(5000) NOT NULL,
template_id UUID NOT NULL, template_id UUID NOT NULL,
info VARCHAR(1000), info VARCHAR(1000),
delivery_methods VARCHAR(255), delivery_methods VARCHAR(255),
@ -56,16 +57,17 @@ CREATE TABLE IF NOT EXISTS notification_request (
originator_entity_type VARCHAR(32), originator_entity_type VARCHAR(32),
rule_id UUID NULL, rule_id UUID NULL,
status VARCHAR(32), status VARCHAR(32),
stats VARCHAR(1000) stats VARCHAR(10000)
); );
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_originator_type_created_time ON notification_request(tenant_id, originator_type, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_originator_type_created_time ON notification_request(tenant_id, originator_type, created_time DESC);
CREATE TABLE IF NOT EXISTS notification ( CREATE TABLE IF NOT EXISTS notification (
id UUID NOT NULL, id UUID NOT NULL,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
request_id UUID NOT NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE, 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, recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
type VARCHAR(255) NOT NULL, type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL, text VARCHAR(1000) NOT NULL,
info VARCHAR(1000), info VARCHAR(1000),
originator_type VARCHAR(32) NOT NULL, originator_type VARCHAR(32) NOT NULL,

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

@ -30,7 +30,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -327,7 +327,7 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private NotificationManager notificationManager; private NotificationCenter notificationCenter;
@Autowired @Autowired
@Getter @Getter

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

@ -25,7 +25,7 @@ import org.bouncycastle.util.Arrays;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -687,8 +687,8 @@ class DefaultTbContext implements TbContext {
} }
@Override @Override
public NotificationManager getNotificationManager() { public NotificationCenter getNotificationCenter() {
return mainCtx.getNotificationManager(); return mainCtx.getNotificationCenter();
} }
@Override @Override

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

@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
@ -60,7 +60,7 @@ public class NotificationController extends BaseController {
private final NotificationService notificationService; private final NotificationService notificationService;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
private final NotificationManager notificationManager; private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
@GetMapping("/notifications") @GetMapping("/notifications")
@ -76,15 +76,21 @@ public class NotificationController extends BaseController {
return notificationService.findNotificationsByUserIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); return notificationService.findNotificationsByUserIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
} }
@PutMapping("/notification/{id}/read") // or maybe to NotificationUpdateRequest for the future @PutMapping("/notification/{id}/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markNotificationAsRead(@PathVariable UUID id, public void markNotificationAsRead(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
NotificationId notificationId = new NotificationId(id); NotificationId notificationId = new NotificationId(id);
notificationManager.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId); notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId);
} }
// delete notification? @DeleteMapping("/notification/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void deleteNotification(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) {
NotificationId notificationId = new NotificationId(id);
notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId);
}
@PostMapping("/notification/request") @PostMapping("/notification/request")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -93,11 +99,11 @@ public class NotificationController extends BaseController {
if (notificationRequest.getId() != null) { if (notificationRequest.getId() != null) {
throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it"); throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it");
} }
notificationRequest.setTenantId(user.getTenantId());
checkEntity(notificationRequest.getId(), notificationRequest, Resource.NOTIFICATION_REQUEST); checkEntity(notificationRequest.getId(), notificationRequest, Resource.NOTIFICATION_REQUEST);
notificationRequest.setOriginatorType(NotificationOriginatorType.ADMIN); notificationRequest.setOriginatorType(NotificationOriginatorType.ADMIN);
notificationRequest.setOriginatorEntityId(user.getId()); notificationRequest.setOriginatorEntityId(user.getId());
notificationRequest.setOriginatorEntity(user);
if (notificationRequest.getInfo() != null && notificationRequest.getInfo().getOriginatorType() != null) { if (notificationRequest.getInfo() != null && notificationRequest.getInfo().getOriginatorType() != null) {
throw new IllegalArgumentException("Unsupported notification info type"); throw new IllegalArgumentException("Unsupported notification info type");
} }
@ -105,7 +111,7 @@ public class NotificationController extends BaseController {
notificationRequest.setStatus(null); notificationRequest.setStatus(null);
notificationRequest.setStats(null); notificationRequest.setStats(null);
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationManager::processNotificationRequest); return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::processNotificationRequest);
} }
@GetMapping("/notification/request/{id}") @GetMapping("/notification/request/{id}")
@ -132,13 +138,13 @@ public class NotificationController extends BaseController {
public void deleteNotificationRequest(@PathVariable UUID id) throws Exception { public void deleteNotificationRequest(@PathVariable UUID id) throws Exception {
NotificationRequestId notificationRequestId = new NotificationRequestId(id); NotificationRequestId notificationRequestId = new NotificationRequestId(id);
NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE); NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationManager::deleteNotificationRequest); doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::deleteNotificationRequest);
} }
@PostMapping("/notification/settings") @PostMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings saveNotificationSettings(@RequestBody NotificationSettings notificationSettings, public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) {
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId();
notificationSettingsService.saveNotificationSettings(tenantId, notificationSettings); notificationSettingsService.saveNotificationSettings(tenantId, notificationSettings);

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

@ -55,6 +55,7 @@ public class NotificationRuleController extends BaseController {
@PostMapping("/rule") @PostMapping("/rule")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule) throws Exception { public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule) throws Exception {
notificationRule.setTenantId(getTenantId());
checkEntity(notificationRule.getId(), notificationRule, Resource.NOTIFICATION_RULE); checkEntity(notificationRule.getId(), notificationRule, Resource.NOTIFICATION_RULE);
return doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule); return doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule);
} }

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

@ -75,6 +75,7 @@ public class NotificationTargetController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget, public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget,
@AuthenticationPrincipal SecurityUser user) throws Exception { @AuthenticationPrincipal SecurityUser user) throws Exception {
notificationTarget.setTenantId(user.getTenantId());
checkEntity(notificationTarget.getId(), notificationTarget, Resource.NOTIFICATION_TARGET); checkEntity(notificationTarget.getId(), notificationTarget, Resource.NOTIFICATION_TARGET);
if (!user.isSystemAdmin()) { if (!user.isSystemAdmin()) {
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration(); NotificationTargetConfig targetConfig = notificationTarget.getConfiguration();

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

@ -79,6 +79,7 @@ public class NotificationTemplateController extends BaseController {
@PostMapping("/template") @PostMapping("/template")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception { public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception {
notificationTemplate.setTenantId(getTenantId());
checkEntity(notificationTemplate.getId(), notificationTemplate, Resource.NOTIFICATION_TEMPLATE); checkEntity(notificationTemplate.getId(), notificationTemplate, Resource.NOTIFICATION_TEMPLATE);
return doSaveAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::saveNotificationTemplate); return doSaveAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::saveNotificationTemplate);
} }

120
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationManager.java → application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
@ -33,12 +33,16 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.AlreadySentException; import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationOriginatorType;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; 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.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus; 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.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
@ -66,13 +70,14 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationManager extends AbstractSubscriptionService implements NotificationManager, NotificationChannel { public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel {
private final NotificationTargetService notificationTargetService; private final NotificationTargetService notificationTargetService;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
@ -90,9 +95,21 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
log.debug("Processing notification request (tenant id: {}, notification targets: {})", tenantId, notificationRequest.getTargets()); log.debug("Processing notification request (tenant id: {}, notification targets: {})", tenantId, notificationRequest.getTargets());
notificationRequest.setTenantId(tenantId); notificationRequest.setTenantId(tenantId);
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
NotificationTemplate notificationTemplate = notificationTemplateService.findNotificationTemplateById(tenantId, notificationRequest.getTemplateId());
notificationRequest.getDeliveryMethods().forEach(deliveryMethod -> { notificationRequest.getDeliveryMethods().forEach(deliveryMethod -> {
if (!settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod) || !settings.getDeliveryMethodsConfigs().get(deliveryMethod).isEnabled()) { if (settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod) &&
throw new IllegalArgumentException("Delivery method " + deliveryMethod + " is not enabled or configured"); !settings.getDeliveryMethodsConfigs().get(deliveryMethod).isEnabled()) {
throw new IllegalArgumentException("Delivery method " + deliveryMethod + " is disabled");
}
if (deliveryMethod == NotificationDeliveryMethod.SLACK) {
if (!settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod)) {
throw new IllegalArgumentException("Slack must be configured in the settings");
}
if (!notificationTemplate.getConfiguration().getTemplates().containsKey(deliveryMethod)) {
throw new IllegalArgumentException("To send notification via Slack, " +
"you need to configure corresponding template");
}
} }
}); });
@ -106,21 +123,22 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
} }
} }
notificationRequest.setStatus(NotificationRequestStatus.PROCESSED); notificationRequest.setStatus(NotificationRequestStatus.SENT);
NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest); NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest);
NotificationProcessingContext ctx = NotificationProcessingContext.builder() NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId) .tenantId(tenantId)
.request(savedNotificationRequest) .request(savedNotificationRequest)
.settings(settings) .settings(settings)
.template(notificationTemplate)
.build(); .build();
ctx.init(notificationTemplateService); ctx.init();
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationTargetId targetId : notificationRequest.getTargets()) { for (NotificationTargetId targetId : notificationRequest.getTargets()) {
DaoUtil.processBatches(pageLink -> { DaoUtil.processBatches(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getOriginatorCustomerId(), targetId, pageLink); return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getCustomerId(), targetId, pageLink);
}, 200, recipientsBatch -> { }, 200, recipientsBatch -> {
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationDeliveryMethod deliveryMethod : savedNotificationRequest.getDeliveryMethods()) { for (NotificationDeliveryMethod deliveryMethod : savedNotificationRequest.getDeliveryMethods()) {
NotificationChannel notificationChannel = channels.get(deliveryMethod); NotificationChannel notificationChannel = channels.get(deliveryMethod);
log.debug("Sending {} notifications for request {} to recipients batch", deliveryMethod, savedNotificationRequest.getId()); log.debug("Sending {} notifications for request {} to recipients batch", deliveryMethod, savedNotificationRequest.getId());
@ -136,17 +154,31 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
results.add(resultFuture); results.add(resultFuture);
} }
} }
Futures.allAsList(results).addListener(() -> {
try {
notificationRequestService.updateNotificationRequestStats(tenantId, savedNotificationRequest.getId(), ctx.getStats());
} catch (Exception e) {
log.error("Failed to update stats for notification request {}", savedNotificationRequest.getId(), e);
}
}, dbCallbackExecutorService);
}); });
} }
Futures.whenAllComplete(results).run(() -> {
NotificationRequestStats stats = ctx.getStats();
try {
notificationRequestService.updateNotificationRequestStats(tenantId, savedNotificationRequest.getId(), stats);
} catch (Exception e) {
log.error("Failed to update stats for notification request {}", savedNotificationRequest.getId(), e);
}
UserId senderId = notificationRequest.getSenderId();
if (senderId != null) {
if (stats.getErrors().isEmpty()) {
int sent = stats.getSent().values().stream().mapToInt(Set::size).sum();
sendBasicNotification(tenantId, senderId, NotificationType.COMPLETED, "Notifications sent",
"All notifications were successfully sent (" + sent + ")");
} else {
int failures = stats.getErrors().values().stream().mapToInt(Map::size).sum();
sendBasicNotification(tenantId, senderId, NotificationType.FAILURE, "Notification failure",
"Some notifications were not sent (" + failures + ")"); // TODO: 'Go to request' button
}
}
}, dbCallbackExecutorService);
return savedNotificationRequest; return savedNotificationRequest;
} }
@ -186,6 +218,7 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
.requestId(request.getId()) .requestId(request.getId())
.recipientId(recipient.getId()) .recipientId(recipient.getId())
.type(ctx.getNotificationTemplate().getNotificationType()) .type(ctx.getNotificationTemplate().getNotificationType())
.subject(ctx.getNotificationTemplate().getNotificationSubject())
.text(text) .text(text)
.info(request.getInfo()) .info(request.getInfo())
.originatorType(request.getOriginatorType()) .originatorType(request.getOriginatorType())
@ -197,7 +230,31 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
log.error("Failed to create notification for recipient {}", recipient.getId(), e); log.error("Failed to create notification for recipient {}", recipient.getId(), e);
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), notification, true);
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, NotificationType type, String subject, String text) {
Notification notification = Notification.builder()
.recipientId(recipientId)
.type(type)
.subject(subject)
.text(text)
.originatorType(NotificationOriginatorType.SYSTEM)
.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 @Override
@ -205,8 +262,25 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) { if (updated) {
log.debug("Marking notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); log.debug("Marking notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
Notification notification = notificationService.findNotificationById(tenantId, notificationId); NotificationUpdate update = NotificationUpdate.builder()
onNotificationUpdate(tenantId, recipientId, notification, false); .notificationId(notificationId)
.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);
} }
} }
@ -233,11 +307,7 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
return notificationRequest; return notificationRequest;
} }
private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew) { private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
NotificationUpdate update = NotificationUpdate.builder()
.notification(notification)
.isNew(isNew)
.build();
log.trace("Submitting notification update for recipient {}: {}", recipientId, update); log.trace("Submitting notification update for recipient {}: {}", recipientId, update);
return Futures.submit(() -> { return Futures.submit(() -> {
forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> { forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
@ -270,7 +340,7 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
} }
@Autowired @Autowired
public void setChannels(List<NotificationChannel> channels, NotificationManager websocketNotificationChannel) { public void setChannels(List<NotificationChannel> channels, NotificationCenter websocketNotificationChannel) {
this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c));
this.channels.put(NotificationDeliveryMethod.WEBSOCKET, (NotificationChannel) websocketNotificationChannel); this.channels.put(NotificationDeliveryMethod.WEBSOCKET, (NotificationChannel) websocketNotificationChannel);
} }

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

@ -23,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.notification.NotificationOriginatorTyp
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.rule.NonConfirmedNotificationEscalation; import org.thingsboard.server.common.data.notification.rule.NotificationEscalation;
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -58,7 +58,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
private final NotificationRuleService notificationRuleService; private final NotificationRuleService notificationRuleService;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
@Autowired @Lazy @Autowired @Lazy
private NotificationManager notificationManager; private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
@Override @Override
@ -72,9 +72,15 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
} }
private ListenableFuture<Void> processAlarmUpdate(TenantId tenantId, Alarm alarm, boolean deleted) { private ListenableFuture<Void> processAlarmUpdate(TenantId tenantId, Alarm alarm, boolean deleted) {
if (alarm.getNotificationRuleId() == null) return Futures.immediateFuture(null); NotificationRuleId ruleId = alarm.getNotificationRuleId();
if (ruleId == null) return Futures.immediateFuture(null);
return notificationExecutor.submit(() -> { return notificationExecutor.submit(() -> {
onAlarmUpdate(tenantId, alarm.getNotificationRuleId(), alarm, deleted); try {
onAlarmUpdate(tenantId, ruleId, alarm, deleted);
} catch (Exception e) {
log.error("Failed to process notification rule {} for alarm {}", ruleId, alarm.getId(), e);
throw e;
}
return null; return null;
}); });
} }
@ -91,20 +97,16 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
} }
for (NotificationRequest notificationRequest : notificationRequests) { for (NotificationRequest notificationRequest : notificationRequests) {
if (notificationRequest.getStatus() == NotificationRequestStatus.SCHEDULED) { if (notificationRequest.getStatus() == NotificationRequestStatus.SCHEDULED) {
notificationManager.deleteNotificationRequest(tenantId, notificationRequest.getId()); notificationCenter.deleteNotificationRequest(tenantId, notificationRequest.getId());
} }
} }
} }
if (notificationRequests.isEmpty()) { if (notificationRequests.isEmpty()) {
NotificationRuleConfig config = notificationRule.getConfiguration(); NotificationRuleConfig config = notificationRule.getConfiguration();
NotificationTargetId initialNotificationTargetId = config.getInitialNotificationTargetId(); for (NotificationEscalation escalation : config.getEscalations()) {
if (initialNotificationTargetId != null) { for (NotificationTargetId targetId : escalation.getNotificationTargets()) {
submitNotificationRequest(tenantId, initialNotificationTargetId, notificationRule, alarm, 0); submitNotificationRequest(tenantId, targetId, notificationRule, alarm, escalation.getDelayInSec());
}
if (config.getEscalationConfig() != null) {
for (NonConfirmedNotificationEscalation escalation : config.getEscalationConfig().getEscalations()) {
submitNotificationRequest(tenantId, escalation.getNotificationTargetId(), notificationRule, alarm, escalation.getDelayInSec());
} }
} }
} else { } else {
@ -113,7 +115,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
NotificationInfo previousNotificationInfo = notificationRequest.getInfo(); NotificationInfo previousNotificationInfo = notificationRequest.getInfo();
if (!previousNotificationInfo.equals(newNotificationInfo)) { if (!previousNotificationInfo.equals(newNotificationInfo)) {
notificationRequest.setInfo(newNotificationInfo); notificationRequest.setInfo(newNotificationInfo);
notificationManager.updateNotificationRequest(tenantId, notificationRequest); notificationCenter.updateNotificationRequest(tenantId, notificationRequest);
} }
} }
} }
@ -129,12 +131,6 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
config.setSendingDelayInSec(delayInSec); config.setSendingDelayInSec(delayInSec);
} }
NotificationInfo notificationInfo = constructNotificationInfo(alarm); NotificationInfo notificationInfo = constructNotificationInfo(alarm);
Map<String, String> templateContext = Map.of(
"alarmType", alarm.getType(),
"alarmId", alarm.getId().toString(),
"alarmOriginatorEntityType", alarm.getOriginator().getEntityType().toString(),
"alarmOriginatorId", alarm.getOriginator().getId().toString()
);
NotificationRequest notificationRequest = NotificationRequest.builder() NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.targets(List.of(targetId)) .targets(List.of(targetId))
@ -145,10 +141,8 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
.ruleId(notificationRule.getId()) .ruleId(notificationRule.getId())
.originatorType(NotificationOriginatorType.ALARM) .originatorType(NotificationOriginatorType.ALARM)
.originatorEntityId(alarm.getId()) .originatorEntityId(alarm.getId())
.originatorEntity(alarm)
.templateContext(templateContext)
.build(); .build();
notificationManager.processNotificationRequest(tenantId, notificationRequest); notificationCenter.processNotificationRequest(tenantId, notificationRequest);
} }
private NotificationInfo constructNotificationInfo(Alarm alarm) { private NotificationInfo constructNotificationInfo(Alarm alarm) {
@ -159,6 +153,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
.alarmOriginator(alarm.getOriginator()) .alarmOriginator(alarm.getOriginator())
.alarmSeverity(alarm.getSeverity()) .alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus()) .alarmStatus(alarm.getStatus())
.customerId(alarm.getCustomerId())
.build(); .build();
} }
@ -173,7 +168,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId(); NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId();
List<NotificationRequestId> scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId); List<NotificationRequestId> scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId);
for (NotificationRequestId notificationRequestId : scheduledForRule) { for (NotificationRequestId notificationRequestId : scheduledForRule) {
notificationManager.deleteNotificationRequest(tenantId, notificationRequestId); notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId);
} }
} }

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

@ -21,12 +21,14 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId; 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.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -56,7 +58,7 @@ import java.util.concurrent.TimeUnit;
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService { public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService {
private final NotificationManager notificationManager; private final NotificationCenter notificationCenter;
private final NotificationRequestService notificationRequestService; private final NotificationRequestService notificationRequestService;
private final SchedulerComponent scheduler; private final SchedulerComponent scheduler;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
@ -107,7 +109,16 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS
if (notificationRequest == null) return; if (notificationRequest == null) return;
notificationExecutor.executeAsync(() -> { notificationExecutor.executeAsync(() -> {
notificationManager.processNotificationRequest(tenantId, notificationRequest); 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, NotificationType.FAILURE, "Notification failure",
"Failed to process scheduled notification (request " + notificationRequest.getId() + "): " + e.getMessage());
}
}
}); });
scheduledNotificationRequests.remove(notificationRequest.getId()); scheduledNotificationRequests.remove(notificationRequest.getId());
}, delayInMs, TimeUnit.MILLISECONDS); }, delayInMs, TimeUnit.MILLISECONDS);

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

@ -19,11 +19,12 @@ import com.google.common.base.Strings;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.AlarmOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationOriginatorType;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
@ -31,7 +32,6 @@ import org.thingsboard.server.common.data.notification.settings.NotificationSett
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; 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.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -41,38 +41,35 @@ public class NotificationProcessingContext {
@Getter @Getter
private final TenantId tenantId; private final TenantId tenantId;
private final HasCustomerId originatorEntity;
private final NotificationSettings settings; private final NotificationSettings settings;
@Getter @Getter
private final NotificationRequest request; private final NotificationRequest request;
private final Map<String, String> additionalTemplateContext;
@Getter @Getter
private NotificationTemplate notificationTemplate; private final NotificationTemplate notificationTemplate;
private Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates; private Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter @Getter
private final NotificationRequestStats stats; private final NotificationRequestStats stats;
@Builder @Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings) { public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings,
NotificationTemplate template) {
this.tenantId = tenantId; this.tenantId = tenantId;
this.originatorEntity = request.getOriginatorEntity();
this.request = request; this.request = request;
this.settings = settings; this.settings = settings;
this.additionalTemplateContext = request.getTemplateContext(); this.notificationTemplate = template;
this.stats = new NotificationRequestStats(); this.stats = new NotificationRequestStats();
} }
public void init(NotificationTemplateService templateService) { public void init() {
notificationTemplate = templateService.findNotificationTemplateById(tenantId, request.getTemplateId()); NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration();
NotificationTemplateConfig config = notificationTemplate.getConfiguration(); templates = templateConfig.getTemplates();
for (NotificationDeliveryMethod deliveryMethod : request.getDeliveryMethods()) { for (NotificationDeliveryMethod deliveryMethod : request.getDeliveryMethods()) {
DeliveryMethodNotificationTemplate template = config.getTemplates().get(deliveryMethod); DeliveryMethodNotificationTemplate template = templates.get(deliveryMethod);
if (StringUtils.isEmpty(template.getBody())) { if (StringUtils.isEmpty(template.getBody())) {
template.setBody(config.getDefaultTextTemplate()); template.setBody(templateConfig.getDefaultTextTemplate());
} }
} }
templates = config.getTemplates();
} }
public <T extends DeliveryMethodNotificationTemplate> T getTemplate(NotificationDeliveryMethod deliveryMethod) { public <T extends DeliveryMethodNotificationTemplate> T getTemplate(NotificationDeliveryMethod deliveryMethod) {
@ -88,14 +85,20 @@ public class NotificationProcessingContext {
templateContext.put("email", recipient.getEmail()); templateContext.put("email", recipient.getEmail());
templateContext.put("firstName", Strings.nullToEmpty(recipient.getFirstName())); templateContext.put("firstName", Strings.nullToEmpty(recipient.getFirstName()));
templateContext.put("lastName", Strings.nullToEmpty(recipient.getLastName())); templateContext.put("lastName", Strings.nullToEmpty(recipient.getLastName()));
if (additionalTemplateContext != null) { if (request.getInfo() != null) {
templateContext.putAll(additionalTemplateContext); templateContext.putAll(request.getInfo().getTemplateData());
} }
return templateContext; return templateContext;
} }
public CustomerId getOriginatorCustomerId() { public CustomerId getCustomerId() {
return originatorEntity != null ? originatorEntity.getCustomerId() : null; CustomerId customerId;
if (request.getOriginatorType() == NotificationOriginatorType.ALARM) {
customerId = ((AlarmOriginatedNotificationInfo) request.getInfo()).getCustomerId();
} else {
customerId = null;
}
return customerId;
} }
} }

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

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification.channels;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -35,8 +36,16 @@ public class EmailNotificationChannel implements NotificationChannel {
@Override @Override
public ListenableFuture<Void> sendNotification(User recipient, String text, NotificationProcessingContext ctx) { public ListenableFuture<Void> sendNotification(User recipient, String text, NotificationProcessingContext ctx) {
EmailDeliveryMethodNotificationTemplate template = ctx.getTemplate(NotificationDeliveryMethod.EMAIL); EmailDeliveryMethodNotificationTemplate template = ctx.getTemplate(NotificationDeliveryMethod.EMAIL);
String subject = template.getSubject();
if (StringUtils.isBlank(subject)) {
subject = ctx.getNotificationTemplate().getNotificationSubject();
}
if (StringUtils.isBlank(subject)) {
subject = ctx.getNotificationTemplate().getNotificationType().getName();
}
String mailSubject = subject;
return executor.submit(() -> { return executor.submit(() -> {
mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), text, template.getSubject()); mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), mailSubject, text);
return null; return null;
}); });
} }

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

@ -20,12 +20,12 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.notification.template.SlackConversation;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.notification.AlreadySentException; import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.template.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext; import org.thingsboard.server.service.notification.NotificationProcessingContext;
@ -56,7 +56,7 @@ public class SlackNotificationChannel implements NotificationChannel {
if (StringUtils.isNoneEmpty(recipient.getFirstName(), recipient.getLastName())) { if (StringUtils.isNoneEmpty(recipient.getFirstName(), recipient.getLastName())) {
String username = StringUtils.join(new String[]{recipient.getFirstName(), recipient.getLastName()}, ' '); String username = StringUtils.join(new String[]{recipient.getFirstName(), recipient.getLastName()}, ' ');
return executor.submit(() -> { return executor.submit(() -> {
SlackConversation conversation = slackService.findConversation(recipient.getTenantId(), config.getBotToken(), SlackConversation.Type.USER, username); SlackConversation conversation = slackService.findConversation(recipient.getTenantId(), config.getBotToken(), SlackConversation.Type.DIRECT, username);
if (conversation == null) { if (conversation == null) {
throw new IllegalArgumentException("Slack user not found for given name '" + username + "'"); throw new IllegalArgumentException("Slack user not found for given name '" + username + "'");
} }

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

@ -68,7 +68,7 @@ public class DefaultSlackService implements SlackService {
@Override @Override
public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversation.Type conversationType) { public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversation.Type conversationType) {
return cache.get(conversationType + ":" + token, k -> { return cache.get(conversationType + ":" + token, k -> {
if (conversationType == SlackConversation.Type.USER) { if (conversationType == SlackConversation.Type.DIRECT) {
UsersListRequest request = UsersListRequest.builder() UsersListRequest request = UsersListRequest.builder()
.limit(CONVERSATIONS_LIMIT) .limit(CONVERSATIONS_LIMIT)
.build(); .build();

85
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -58,7 +58,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private final NotificationService notificationService; private final NotificationService notificationService;
private final TbLocalSubscriptionService localSubscriptionService; private final TbLocalSubscriptionService localSubscriptionService;
private final NotificationManager notificationManager; private final NotificationCenter notificationCenter;
private final TbServiceInfoProvider serviceInfoProvider; private final TbServiceInfoProvider serviceInfoProvider;
@Autowired @Lazy @Autowired @Lazy
private WebSocketService wsService; private WebSocketService wsService;
@ -130,29 +130,45 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) { private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification(); Notification notification = update.getNotification();
if (update.isNew()) { UUID notificationId = update.getNotificationId().getId();
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); switch (update.getUpdateType()) {
subscription.getTotalUnreadCounter().incrementAndGet(); case CREATED: {
if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) { subscription.getLatestUnreadNotifications().put(notificationId, notification);
Set<UUID> beyondLimit = subscription.getSortedNotifications().stream().skip(subscription.getLimit()) subscription.getTotalUnreadCounter().incrementAndGet();
.map(IdBased::getUuidId).collect(Collectors.toSet()); if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) {
beyondLimit.forEach(notificationId -> subscription.getLatestUnreadNotifications().remove(notificationId)); Set<UUID> 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;
} }
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification)); case UPDATED: {
} else { if (update.getUpdatedStatus() == NotificationStatus.READ) {
if (notification.getStatus() != NotificationStatus.READ) { if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
if (subscription.getLatestUnreadNotifications().containsKey(notification.getUuidId())) { fetchUnreadNotifications(subscription);
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification)); } else {
subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
}
} else if (notification.getStatus() != NotificationStatus.READ) {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
subscription.getLatestUnreadNotifications().put(notificationId, notification);
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
}
} }
} else { break;
if (subscription.getLatestUnreadNotifications().containsKey(notification.getUuidId())) { }
case DELETED: {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
fetchUnreadNotifications(subscription); fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} else { } else if (notification.getStatus() != NotificationStatus.READ) {
subscription.getTotalUnreadCounter().decrementAndGet(); subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createCountUpdate()); sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
} }
break;
} }
} }
} }
@ -162,13 +178,13 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
NotificationRequestId notificationRequestId = update.getNotificationRequestId(); NotificationRequestId notificationRequestId = update.getNotificationRequestId();
if (update.isDeleted()) { if (update.isDeleted()) {
if (subscription.getLatestUnreadNotifications().values().stream() if (subscription.getLatestUnreadNotifications().values().stream()
.anyMatch(notification -> notification.getRequestId().equals(notificationRequestId))) { .anyMatch(notification -> notificationRequestId.equals(notification.getRequestId()))) {
fetchUnreadNotifications(subscription); fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} }
} else { } else {
subscription.getLatestUnreadNotifications().values().stream() subscription.getLatestUnreadNotifications().values().stream()
.filter(notification -> notification.getRequestId().equals(notificationRequestId)) .filter(notification -> notificationRequestId.equals(notification.getRequestId()))
.forEach(notification -> { .forEach(notification -> {
notification.setInfo(update.getNotificationInfo()); notification.setInfo(update.getNotificationInfo());
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification)); sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
@ -189,13 +205,28 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) { private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update); log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification(); Notification notification = update.getNotification();
if (update.isNew()) { switch (update.getUpdateType()) {
subscription.getUnreadCounter().incrementAndGet(); case CREATED: {
} else if (notification.getStatus() == NotificationStatus.READ) { System.err.println("NotificationsCountSubscription CREATED");
// for now this can only happen when user marks notification as read subscription.getUnreadCounter().incrementAndGet();
subscription.getUnreadCounter().decrementAndGet(); sendUpdate(subscription.getSessionId(), subscription.createUpdate());
break;
}
case UPDATED: {
if (update.getUpdatedStatus() == NotificationStatus.READ) {
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;
}
} }
sendUpdate(subscription.getSessionId(), subscription.createUpdate());
} }
private void handleNotificationRequestUpdate(NotificationsCountSubscription subscription, NotificationRequestUpdate update) { private void handleNotificationRequestUpdate(NotificationsCountSubscription subscription, NotificationRequestUpdate update) {
@ -219,7 +250,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
cmd.getNotifications().stream() cmd.getNotifications().stream()
.map(NotificationId::new) .map(NotificationId::new)
.forEach(notificationId -> { .forEach(notificationId -> {
notificationManager.markNotificationAsRead(securityCtx.getTenantId(), securityCtx.getId(), notificationId); notificationCenter.markNotificationAsRead(securityCtx.getTenantId(), securityCtx.getId(), notificationId);
// fixme: should send bulk update event, not a separate event for each notification // fixme: should send bulk update event, not a separate event for each notification
}); });
} }

13
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationUpdate.java

@ -19,13 +19,24 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; 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.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class NotificationUpdate { public class NotificationUpdate {
private NotificationId notificationId;
private NotificationStatus updatedStatus;
private Notification notification; private Notification notification;
private boolean isNew; private ComponentLifecycleEvent updateType;
public NotificationId getNotificationId() {
return notificationId != null ? notificationId : notification.getId();
}
} }

21
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -53,9 +53,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
import org.thingsboard.server.common.data.Customer; 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.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType; 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.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -80,6 +82,8 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority; 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.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
@ -111,6 +115,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
@Slf4j @Slf4j
public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
public static final int TIMEOUT = 30; public static final int TIMEOUT = 30;
protected ObjectMapper mapper = new ObjectMapper(); protected ObjectMapper mapper = new ObjectMapper();
@ -483,6 +488,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return protoTransportPayloadConfiguration; 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 { protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
@ -584,7 +601,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass);
} }
protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) { protected <T, R> R doPost(String urlTemplate, T content, Class<R> responseClass, String... params) {
try { try {
return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) { } catch (Exception e) {
@ -694,10 +711,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
} }
public class IdComparator<D extends HasId> implements Comparator<D> { public class IdComparator<D extends HasId> implements Comparator<D> {
@Override @Override
public int compare(D o1, D o2) { public int compare(D o1, D o2) {
return o1.getId().getId().compareTo(o2.getId().getId()); return o1.getId().getId().compareTo(o2.getId().getId());
} }
} }
protected static <T> ResultMatcher statusReason(Matcher<T> matcher) { protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {

178
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -0,0 +1,178 @@
/**
* Copyright © 2016-2022 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.UserId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationInfo;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
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.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 static org.assertj.core.api.Assertions.assertThat;
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.ADMIN;
protected NotificationTarget createNotificationTarget(UserId... usersIds) {
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setTenantId(tenantId);
notificationTarget.setName("Users " + List.of(usersIds));
UserListNotificationTargetConfig config = new UserListNotificationTargetConfig();
config.setUsersIds(DaoUtil.toUUIDs(List.of(usersIds)));
notificationTarget.setConfiguration(config);
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<NotificationTargetId> targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) {
if (deliveryMethods.length == 0) {
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEBSOCKET};
}
NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods);
NotificationRequestConfig config = new NotificationRequestConfig();
config.setSendingDelayInSec(delayInSec);
NotificationInfo notificationInfo = new NotificationInfo();
notificationInfo.setDescription("The text: " + text);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.targets(targets)
.templateId(notificationTemplate.getId())
.info(notificationInfo)
.deliveryMethods(List.of(deliveryMethods))
.additionalConfig(config)
.build();
return doPost("/api/notification/request", notificationRequest, NotificationRequest.class);
}
protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setTenantId(tenantId);
notificationTemplate.setName("Notification template for testing");
notificationTemplate.setNotificationType(notificationType);
notificationTemplate.setNotificationSubject(subject);
NotificationTemplateConfig config = new NotificationTemplateConfig();
config.setDefaultTextTemplate(text);
config.setTemplates(new HashMap<>());
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod == NotificationDeliveryMethod.EMAIL) {
EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
emailNotificationTemplate.setSubject("Hello from test");
emailNotificationTemplate.setMethod(deliveryMethod);
config.getTemplates().put(deliveryMethod, emailNotificationTemplate);
} else {
DeliveryMethodNotificationTemplate defaultTemplate = new DeliveryMethodNotificationTemplate();
defaultTemplate.setMethod(deliveryMethod);
config.getTemplates().put(deliveryMethod, defaultTemplate);
}
}
notificationTemplate.setConfiguration(config);
return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
}
protected Pair<User, NotificationApiWsClient> 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 NotificationRequest findNotificationRequest(NotificationRequestId id) throws Exception {
return doGet("/api/notification/request/" + id, NotificationRequest.class);
}
protected void deleteNotificationRequest(NotificationRequestId id) throws Exception {
doDelete("/api/notification/request/" + id);
}
protected List<Notification> getMyNotifications(boolean unreadOnly, int limit) throws Exception {
return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference<PageData<Notification>>() {},
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;
}
protected void connectWsClient() throws Exception {
loginCustomerUser();
wsClient = (NotificationApiWsClient) super.getWsClient();
loginTenantAdmin();
}
protected void connectOtherWsClient() throws Exception {
loginCustomerUser();
otherWsClient = (NotificationApiWsClient) super.getAnotherWsClient();
loginTenantAdmin();
}
}

425
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -15,42 +15,29 @@
*/ */
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.assertj.core.data.Offset; import org.assertj.core.data.Offset;
import org.java_websocket.client.WebSocketClient; import org.java_websocket.client.WebSocketClient;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.rule.engine.api.NotificationManager; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.User; 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.NotificationTargetId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationInfo; import org.thingsboard.server.common.data.notification.NotificationInfo;
import org.thingsboard.server.common.data.notification.NotificationRequest; 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.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.SingleUserNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
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.DeliveryMethodNotificationTemplate;
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.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URISyntaxException; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -59,228 +46,252 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@DaoSqlTest @DaoSqlTest
@Slf4j @Slf4j
public class NotificationApiTest extends AbstractControllerTest { public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationManager notificationManager; private NotificationCenter notificationCenter;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
loginTenantAdmin(); connectWsClient();
} }
@Test @Test
public void testSubscribingToUnreadNotificationsCount() { public void testSubscribingToUnreadNotificationsCount() {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText1 = "Notification 1"; String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1); submitNotificationRequest(notificationTarget.getId(), notificationText1);
String notificationText2 = "Notification 2"; String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2); submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().subscribeForUnreadNotificationsCount(); wsClient.subscribeForUnreadNotificationsCount();
getWsClient().waitForReply(true); wsClient.waitForReply(true);
UnreadNotificationsCountUpdate update = getWsClient().getLastCountUpdate(); UnreadNotificationsCountUpdate update = wsClient.getLastCountUpdate();
assertThat(update.getTotalUnreadCount()).isEqualTo(2); assertThat(update.getTotalUnreadCount()).isEqualTo(2);
} }
@Test @Test
public void testReceivingCountUpdates_multipleSessions() { public void testReceivingCountUpdates_multipleSessions() throws Exception {
getWsClient().subscribeForUnreadNotificationsCount(); connectOtherWsClient();
getAnotherWsClient().subscribeForUnreadNotificationsCount(); wsClient.subscribeForUnreadNotificationsCount();
getWsClient().waitForReply(true); otherWsClient.subscribeForUnreadNotificationsCount();
getAnotherWsClient().waitForReply(true); wsClient.waitForReply(true);
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isZero(); otherWsClient.waitForReply(true);
assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); otherWsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Notification"; String notificationText = "Notification";
submitNotificationRequest(notificationTarget.getId(), notificationText); submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true); otherWsClient.waitForUpdate(true);
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne(); assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne(); assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
} }
@Test @Test
public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception { public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText1 = "Notification 1"; String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1); submitNotificationRequest(notificationTarget.getId(), notificationText1);
String notificationText2 = "Notification 2"; String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2); submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().subscribeForUnreadNotifications(10); connectOtherWsClient();
getAnotherWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); otherWsClient.subscribeForUnreadNotifications(10);
getAnotherWsClient().waitForReply(true); wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText1, notificationText2); checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText1, notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText1, notificationText2); checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText1, notificationText2);
} }
@Test @Test
public void testReceivingNotificationUpdates_multipleSessions() { public void testReceivingNotificationUpdates_multipleSessions() throws Exception {
getWsClient().subscribeForUnreadNotifications(10); connectOtherWsClient();
getAnotherWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); otherWsClient.subscribeForUnreadNotifications(10);
getAnotherWsClient().waitForReply(true); wsClient.waitForReply(true);
UnreadNotificationsUpdate notificationsUpdate = getWsClient().getLastDataUpdate(); otherWsClient.waitForReply(true);
UnreadNotificationsUpdate notificationsUpdate = wsClient.getLastDataUpdate();
assertThat(notificationsUpdate.getTotalUnreadCount()).isZero(); assertThat(notificationsUpdate.getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(); otherWsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Notification 1"; String notificationText = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText); submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true); otherWsClient.waitForUpdate(true);
checkPartialNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText, 1); checkPartialNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText, 1);
checkPartialNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText, 1); checkPartialNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText, 1);
} }
@Test @Test
public void testMarkingAsRead_multipleSessions() { public void testMarkingAsRead_multipleSessions() throws Exception {
getWsClient().subscribeForUnreadNotifications(10); connectOtherWsClient();
getAnotherWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); otherWsClient.subscribeForUnreadNotifications(10);
getAnotherWsClient().waitForReply(true); wsClient.waitForReply(true);
getAnotherWsClient().subscribeForUnreadNotificationsCount(); otherWsClient.waitForReply(true);
getAnotherWsClient().waitForReply(true); otherWsClient.subscribeForUnreadNotificationsCount();
otherWsClient.waitForReply(true);
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
getWsClient().registerWaitForUpdate(); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
getAnotherWsClient().registerWaitForUpdate(2); wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate(2);
String notificationText1 = "Notification 1"; String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1); submitNotificationRequest(notificationTarget.getId(), notificationText1);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true); otherWsClient.waitForUpdate(true);
Notification notification1 = getWsClient().getLastDataUpdate().getUpdate(); Notification notification1 = wsClient.getLastDataUpdate().getUpdate();
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2); otherWsClient.registerWaitForUpdate(2);
String notificationText2 = "Notification 2"; String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2); submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true); otherWsClient.waitForUpdate(true);
assertThat(getWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2); assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2); assertThat(otherWsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(2); assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isEqualTo(2);
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2); otherWsClient.registerWaitForUpdate(2);
getWsClient().markNotificationAsRead(notification1.getUuidId()); wsClient.markNotificationAsRead(notification1.getUuidId());
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true); otherWsClient.waitForUpdate(true);
checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText2); checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText2); checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne(); assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
} }
@Test @Test
public void testDelayedNotificationRequest() throws Exception { public void testDelayedNotificationRequest() throws Exception {
getWsClient().subscribeForUnreadNotifications(5); wsClient.subscribeForUnreadNotifications(5);
getWsClient().waitForReply(true); wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Was scheduled for 5 sec"; String notificationText = "Was scheduled for 5 sec";
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5); NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5);
assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED); assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED);
await().atLeast(4, TimeUnit.SECONDS) await().atLeast(4, TimeUnit.SECONDS)
.atMost(6, TimeUnit.SECONDS) .atMost(6, TimeUnit.SECONDS)
.until(() -> getWsClient().getLastMsg() != null); .until(() -> wsClient.getLastMsg() != null);
Notification delayedNotification = getWsClient().getLastDataUpdate().getUpdate(); Notification delayedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText); assertThat(delayedNotification).extracting(Notification::getText).isEqualTo(notificationText);
assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime()) assertThat(delayedNotification.getCreatedTime() - notificationRequest.getCreatedTime())
.isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(500L)); .isCloseTo(TimeUnit.SECONDS.toMillis(5), Offset.offset(500L));
assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.PROCESSED); assertThat(findNotificationRequest(notificationRequest.getId()).getStatus()).isEqualTo(NotificationRequestStatus.SENT);
} }
@Test @Test
public void whenNotificationRequestIsDeleted_thenDeleteNotifications() throws Exception { public void whenNotificationRequestIsDeleted_thenDeleteNotifications() throws Exception {
getWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test"); NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test");
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
assertThat(getWsClient().getNotifications()).singleElement().extracting(Notification::getRequestId) assertThat(wsClient.getNotifications()).singleElement().extracting(Notification::getRequestId)
.isEqualTo(notificationRequest.getId()); .isEqualTo(notificationRequest.getId());
assertThat(getWsClient().getUnreadCount()).isOne(); assertThat(wsClient.getUnreadCount()).isOne();
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
deleteNotificationRequest(notificationRequest.getId()); deleteNotificationRequest(notificationRequest.getId());
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
assertThat(getWsClient().getNotifications()).isEmpty(); assertThat(wsClient.getNotifications()).isEmpty();
assertThat(getWsClient().getUnreadCount()).isZero(); assertThat(wsClient.getUnreadCount()).isZero();
loginCustomerUser();
assertThat(getMyNotifications(false, 10)).size().isZero(); assertThat(getMyNotifications(false, 10)).size().isZero();
} }
@Test @Test
public void whenNotificationRequestIsUpdated_thenUpdateNotifications() throws Exception { public void whenNotificationRequestIsUpdated_thenUpdateNotifications() throws Exception {
getWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); wsClient.waitForReply(true);
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Text"; String notificationText = "Text";
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText); NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
Notification initialNotification = getWsClient().getLastDataUpdate().getUpdate(); Notification initialNotification = wsClient.getLastDataUpdate().getUpdate();
loginCustomerUser();
assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(initialNotification); assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(initialNotification);
assertThat(initialNotification.getInfo()).isNotNull().isEqualTo(notificationRequest.getInfo()); assertThat(initialNotification.getInfo()).isNotNull().isEqualTo(notificationRequest.getInfo());
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationInfo newNotificationInfo = new NotificationInfo(); NotificationInfo newNotificationInfo = new NotificationInfo();
newNotificationInfo.setDescription("New description"); newNotificationInfo.setDescription("New description");
notificationRequest.setInfo(newNotificationInfo); notificationRequest.setInfo(newNotificationInfo);
notificationManager.updateNotificationRequest(tenantId, notificationRequest); notificationCenter.updateNotificationRequest(tenantId, notificationRequest);
getWsClient().waitForUpdate(true); wsClient.waitForUpdate(true);
Notification updatedNotification = getWsClient().getLastDataUpdate().getUpdate(); Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(updatedNotification.getInfo()).isEqualTo(newNotificationInfo); assertThat(updatedNotification.getInfo()).isEqualTo(newNotificationInfo);
assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(updatedNotification); assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(updatedNotification);
} }
@Test @Test
public void testNotificationUpdatesForALotOfUsers() throws Exception { public void testNotificationUpdatesForALotOfUsers() throws Exception {
int usersCount = 100; int usersCount = 100; // FIXME: sometimes if set e.g. to 150, up to 5 WS sessions don't receive update
Map<User, NotificationApiWsClient> wsSessions = createUsersAndSetUpWsSessions(usersCount); Map<User, NotificationApiWsClient> sessions = new HashMap<>();
wsSessions.values().forEach(wsClient -> { List<NotificationTargetId> 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(2);
wsClient.subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
wsClient.subscribeForUnreadNotificationsCount(); wsClient.subscribeForUnreadNotificationsCount();
wsClient.waitForReply(true); }
}); sessions.values().forEach(wsClient -> wsClient.waitForUpdate(true));
loginTenantAdmin();
NotificationTarget notificationTarget = new NotificationTarget(); sessions.forEach((user, wsClient) -> wsClient.registerWaitForUpdate(2));
UserListNotificationTargetConfig config = new UserListNotificationTargetConfig(); NotificationRequest notificationRequest = submitNotificationRequest(targets, "Hello, ${email}", 0,
config.setUsersIds(wsSessions.keySet().stream().map(User::getUuidId).collect(Collectors.toList())); NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL);
notificationTarget.setName("Test users");
notificationTarget.setTenantId(tenantId);
notificationTarget.setConfiguration(config);
notificationTarget = saveNotificationTarget(notificationTarget);
wsSessions.forEach((user, wsClient) -> wsClient.registerWaitForUpdate(2));
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Hello, ${email}",
NotificationDeliveryMethod.WEBSOCKET);
await().atMost(20, TimeUnit.SECONDS) await().atMost(20, TimeUnit.SECONDS)
.pollDelay(1, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS) .pollDelay(1, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> wsSessions.values().stream() .until(() -> {
.allMatch(wsClient -> wsClient.getLastDataUpdate() != null long receivedUpdate = sessions.values().stream()
&& wsClient.getLastCountUpdate() != null)); .filter(wsClient -> wsClient.getLastDataUpdate() != null
&& wsClient.getLastCountUpdate() != null)
wsSessions.forEach((user, wsClient) -> { .count();
System.err.println("WS sessions received update: " + receivedUpdate);
return receivedUpdate == sessions.size();
});
verify(mailService, timeout(1500).times(usersCount)).sendEmail(eq(tenantId), startsWith("test-user-"), any(), any());
sessions.forEach((user, wsClient) -> {
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isOne(); assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isOne();
assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isOne(); assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
@ -293,148 +304,56 @@ public class NotificationApiTest extends AbstractControllerTest {
await().atMost(2, TimeUnit.SECONDS) await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null); .until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null);
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats(); NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats();
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET)).hasValue(usersCount); assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET))
.containsAll(sessions.keySet().stream().map(User::getEmail).collect(Collectors.toSet()));
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL))
.containsAll(sessions.keySet().stream().map(User::getEmail).collect(Collectors.toSet()));
wsSessions.values().forEach(WebSocketClient::close); sessions.values().forEach(wsClient -> wsClient.registerWaitForUpdate(2));
} deleteNotificationRequest(notificationRequest.getId());
sessions.values().forEach(wsClient -> {
wsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getNotifications()).isEmpty();
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isZero();
assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isZero();
});
private Map<User, NotificationApiWsClient> createUsersAndSetUpWsSessions(int count) throws Exception { sessions.values().forEach(WebSocketClient::close);
Map<User, NotificationApiWsClient> wsSessions = new HashMap<>();
for (int i = 1; i <= count; 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 = (NotificationApiWsClient) buildAndConnectWebSocketClient();
wsSessions.put(user, wsClient);
}
loginTenantAdmin();
return wsSessions;
} }
@Test @Test
public void testNotificationRequestStats() throws Exception { public void testNotificationRequestStats() throws Exception {
getWsClient().subscribeForUnreadNotifications(10); wsClient.subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true); wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate(); wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test :)", NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test :)",
NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS); NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS);
getWsClient().waitForUpdate(); wsClient.waitForUpdate();
await().atMost(2, TimeUnit.SECONDS) await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null); .until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null);
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats(); NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats();
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET)).hasValue(1); assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET)).containsOnly(CUSTOMER_USER_EMAIL);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(1); assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).containsOnly(CUSTOMER_USER_EMAIL);
assertThat(stats.getErrors().get(NotificationDeliveryMethod.SMS)).size().isOne(); assertThat(stats.getErrors().get(NotificationDeliveryMethod.SMS)).size().isOne();
System.err.println(stats);
} }
private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) {
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(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); assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedNotifications.length);
} }
private void checkPartialNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String expectedNotification, int expectedUnreadCount) { private void checkPartialNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String expectedNotification, int expectedUnreadCount) {
assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getText).isEqualTo(expectedNotification); 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); assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedUnreadCount);
} }
private NotificationTarget createNotificationTarget(UserId userId) {
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setTenantId(tenantId);
notificationTarget.setName("User " + userId);
SingleUserNotificationTargetConfig config = new SingleUserNotificationTargetConfig();
config.setUserId(userId.getId());
notificationTarget.setConfiguration(config);
return saveNotificationTarget(notificationTarget);
}
private NotificationTarget saveNotificationTarget(NotificationTarget notificationTarget) {
return doPost("/api/notification/target", notificationTarget, NotificationTarget.class);
}
private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, NotificationDeliveryMethod... deliveryMethods) {
return submitNotificationRequest(targetId, text, 0, deliveryMethods);
}
private NotificationRequest submitNotificationRequest(NotificationTargetId targetId, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) {
if (deliveryMethods.length == 0) {
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEBSOCKET};
}
NotificationTemplate notificationTemplate = createNotificationTemplate(text, deliveryMethods);
NotificationRequestConfig config = new NotificationRequestConfig();
config.setSendingDelayInSec(delayInSec);
NotificationInfo notificationInfo = new NotificationInfo();
notificationInfo.setDescription("The text: " + text);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.targets(List.of(targetId))
.templateId(notificationTemplate.getId())
.info(notificationInfo)
.deliveryMethods(List.of(deliveryMethods))
.additionalConfig(config)
.build();
return doPost("/api/notification/request", notificationRequest, NotificationRequest.class);
}
private NotificationTemplate createNotificationTemplate(String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setTenantId(tenantId);
notificationTemplate.setName("Notification template for testing");
notificationTemplate.setNotificationType("Just a test");
NotificationTemplateConfig config = new NotificationTemplateConfig();
config.setDefaultTextTemplate(text);
config.setTemplates(new HashMap<>());
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod == NotificationDeliveryMethod.EMAIL) {
EmailDeliveryMethodNotificationTemplate emailNotificationTemplate = new EmailDeliveryMethodNotificationTemplate();
emailNotificationTemplate.setSubject("Hello from test");
emailNotificationTemplate.setMethod(deliveryMethod);
config.getTemplates().put(deliveryMethod, emailNotificationTemplate);
} else {
DeliveryMethodNotificationTemplate defaultTemplate = new DeliveryMethodNotificationTemplate();
defaultTemplate.setMethod(deliveryMethod);
config.getTemplates().put(deliveryMethod, defaultTemplate);
}
}
notificationTemplate.setConfiguration(config);
return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
}
private NotificationRequest findNotificationRequest(NotificationRequestId id) throws Exception {
return doGet("/api/notification/request/" + id, NotificationRequest.class);
}
private void deleteNotificationRequest(NotificationRequestId id) throws Exception {
doDelete("/api/notification/request/" + id);
}
private List<Notification> getMyNotifications(boolean unreadOnly, int limit) throws Exception {
return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference<PageData<Notification>>() {},
new PageLink(limit, 0), unreadOnly).getData();
}
@Override
protected TbTestWebSocketClient 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();
}
@Override
public NotificationApiWsClient getAnotherWsClient() {
return (NotificationApiWsClient) super.getAnotherWsClient();
}
} }

198
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -0,0 +1,198 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
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.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.notification.AlarmOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.NotificationEscalation;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
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.security.Authority;
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.TreeMap;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
@DaoSqlTest
public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
private AlarmSubscriptionService alarmSubscriptionService;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
}
@Test
public void testNotificationRuleProcessing() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL};
NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, "New alarm", "NEW ALARM ${alarmType}", deliveryMethods);
NotificationRule notificationRule = new NotificationRule();
notificationRule.setTenantId(tenantId);
notificationRule.setName("Test rule for my alarms");
notificationRule.setTemplateId(notificationTemplate.getId());
notificationRule.setDeliveryMethods(List.of(deliveryMethods));
NotificationRuleConfig config = new NotificationRuleConfig();
List<NotificationEscalation> escalations = new ArrayList<>();
Map<Integer, NotificationApiWsClient> clients = new HashMap<>();
for (int delay = 0; delay <= 5; delay++) {
Pair<User, NotificationApiWsClient> userAndClient = createUserAndConnectWsClient(Authority.TENANT_ADMIN);
NotificationTarget notificationTarget = createNotificationTarget(userAndClient.getFirst().getId());
NotificationEscalation escalation = new NotificationEscalation();
escalation.setDelayInSec(delay);
escalation.setNotificationTargets(List.of(notificationTarget.getId()));
escalations.add(escalation);
clients.put(delay, userAndClient.getSecond());
}
config.setEscalations(escalations);
notificationRule.setConfiguration(config);
notificationRule = saveNotificationRule(notificationRule);
String alarmType = "boolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(notificationRule.getId(), alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
clients.values().forEach(wsClient -> {
wsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
});
JsonNode attr = JacksonUtil.newObjectNode()
.set("bool", BooleanNode.TRUE);
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
verify(alarmSubscriptionService, timeout(2000)).createOrUpdateAlarm(argThat(alarm -> alarm.getType().equals(alarmType)));
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
assertThat(alarm.getNotificationRuleId()).isEqualTo(notificationRule.getId());
long ts = System.currentTimeMillis();
await().atMost(7, TimeUnit.SECONDS)
.until(() -> clients.values().stream().allMatch(client -> client.getLastDataUpdate() != null));
clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate();
double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000;
assertThat(actualDelay).isCloseTo(expectedDelay, offset(0.5));
assertThat(notification.getText()).isEqualTo("NEW ALARM " + alarm.getType());
assertThat(notification.getType()).isEqualTo(NotificationType.ALARM);
assertThat(notification.getSubject()).isEqualTo("New alarm");
assertThat(notification.getInfo()).isInstanceOf(AlarmOriginatedNotificationInfo.class);
AlarmOriginatedNotificationInfo info = (AlarmOriginatedNotificationInfo) notification.getInfo();
assertThat(info.getAlarmId()).isEqualTo(alarm.getId());
assertThat(info.getAlarmType()).isEqualTo(alarm.getType());
assertThat(info.getAlarmSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(info.getAlarmStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
clients.values().forEach(wsClient -> wsClient.registerWaitForUpdate());
alarmSubscriptionService.ackAlarm(tenantId, alarm.getId(), System.currentTimeMillis());
clients.values().forEach(wsClient -> {
wsClient.waitForUpdate(true);
Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(((AlarmOriginatedNotificationInfo) updatedNotification.getInfo()).getAlarmStatus())
.isEqualTo(AlarmStatus.ACTIVE_ACK);
wsClient.close();
});
}
private DeviceProfile createDeviceProfileWithAlarmRules(NotificationRuleId notificationRuleId, String alarmType) {
DeviceProfile deviceProfile = createDeviceProfile("For notification rule test");
deviceProfile.setTenantId(tenantId);
List<DeviceProfileAlarm> alarms = new ArrayList<>();
DeviceProfileAlarm alarm = new DeviceProfileAlarm();
alarm.setAlarmType(alarmType);
alarm.setId(alarmType);
alarm.setNotificationRuleId(notificationRuleId);
AlarmRule alarmRule = new AlarmRule();
alarmRule.setAlarmDetails("Details");
AlarmCondition alarmCondition = new AlarmCondition();
alarmCondition.setSpec(new SimpleAlarmConditionSpec());
List<AlarmConditionFilter> 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<AlarmSeverity, AlarmRule> 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 NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
}

16
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.AllUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.AllUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.CustomerUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.CustomerUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.SingleUserNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
@ -62,14 +61,6 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
.contains("name must not be") .contains("name must not be")
.contains("configuration must not be"); .contains("configuration must not be");
SingleUserNotificationTargetConfig singleUserConfig = new SingleUserNotificationTargetConfig();
singleUserConfig.setUserId(null);
notificationTarget.setConfiguration(singleUserConfig);
validationError = saveAndGetError(notificationTarget, status().isBadRequest());
assertThat(validationError)
.contains("userId must not be");
UserListNotificationTargetConfig userListConfig = new UserListNotificationTargetConfig(); UserListNotificationTargetConfig userListConfig = new UserListNotificationTargetConfig();
userListConfig.setUsersIds(Collections.emptyList()); userListConfig.setUsersIds(Collections.emptyList());
notificationTarget.setConfiguration(userListConfig); notificationTarget.setConfiguration(userListConfig);
@ -85,18 +76,13 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
NotificationTarget notificationTarget = new NotificationTarget(); NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setTenantId(differentTenantId); notificationTarget.setTenantId(differentTenantId);
notificationTarget.setName("Target 1"); notificationTarget.setName("Target 1");
UserListNotificationTargetConfig userListConfig = new UserListNotificationTargetConfig(); UserListNotificationTargetConfig userListConfig = new UserListNotificationTargetConfig();
userListConfig.setUsersIds(List.of(customerUserId.getId(), tenantAdminUserId.getId())); userListConfig.setUsersIds(List.of(customerUserId.getId(), tenantAdminUserId.getId()));
notificationTarget.setConfiguration(userListConfig); notificationTarget.setConfiguration(userListConfig);
saveAndGetError(notificationTarget, status().isForbidden()); saveAndGetError(notificationTarget, status().isForbidden());
SingleUserNotificationTargetConfig singleUserConfig = new SingleUserNotificationTargetConfig();
singleUserConfig.setUserId(customerUserId.getId());
notificationTarget.setConfiguration(singleUserConfig);
saveAndGetError(notificationTarget, status().isForbidden());
loginSysAdmin(); loginSysAdmin();
notificationTarget.setTenantId(TenantId.SYS_TENANT_ID); notificationTarget.setTenantId(TenantId.SYS_TENANT_ID);
notificationTarget.setConfiguration(userListConfig); notificationTarget.setConfiguration(userListConfig);

2
application/src/test/java/org/thingsboard/server/service/notification/NotificationTemplateApiTest.java

@ -45,6 +45,7 @@ public class NotificationTemplateApiTest extends AbstractControllerTest {
notificationTemplate.setTenantId(tenantId); notificationTemplate.setTenantId(tenantId);
notificationTemplate.setName(null); notificationTemplate.setName(null);
notificationTemplate.setNotificationType(null); notificationTemplate.setNotificationType(null);
notificationTemplate.setNotificationSubject(null);
notificationTemplate.setConfiguration(null); notificationTemplate.setConfiguration(null);
String validationError = saveAndGetError(notificationTemplate, status().isBadRequest()); String validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
@ -68,7 +69,6 @@ public class NotificationTemplateApiTest extends AbstractControllerTest {
validationError = saveAndGetError(notificationTemplate, status().isBadRequest()); validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
assertThat(validationError) assertThat(validationError)
.doesNotContain("defaultTextTemplate must be specified") .doesNotContain("defaultTextTemplate must be specified")
.contains("subject must not be")
.contains("name is malformed"); .contains("name is malformed");
config.setDefaultTextTemplate(null); config.setDefaultTextTemplate(null);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java

@ -36,4 +36,6 @@ public interface NotificationService {
int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId); int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId);
boolean deleteNotification(TenantId tenantId, UserId userId, NotificationId notificationId);
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -89,6 +89,8 @@ public class EntityIdFactory {
return new NotificationRuleId(uuid); return new NotificationRuleId(uuid);
case NOTIFICATION_TEMPLATE: case NOTIFICATION_TEMPLATE:
return new NotificationTemplateId(uuid); return new NotificationTemplateId(uuid);
case NOTIFICATION:
return new NotificationId(uuid);
} }
throw new IllegalArgumentException("EntityType " + type + " is not supported!"); throw new IllegalArgumentException("EntityType " + type + " is not supported!");
} }

14
common/data/src/main/java/org/thingsboard/server/common/data/notification/AlarmOriginatedNotificationInfo.java

@ -23,8 +23,11 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor @NoArgsConstructor
@ -37,10 +40,21 @@ public class AlarmOriginatedNotificationInfo extends NotificationInfo {
private EntityId alarmOriginator; private EntityId alarmOriginator;
private AlarmSeverity alarmSeverity; private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus; private AlarmStatus alarmStatus;
private CustomerId customerId;
@Override @Override
public NotificationOriginatorType getOriginatorType() { public NotificationOriginatorType getOriginatorType() {
return NotificationOriginatorType.ALARM; return NotificationOriginatorType.ALARM;
} }
@Override
public Map<String, String> getTemplateData() {
return Map.of(
"alarmType", alarmType,
"alarmId", alarmId.toString(),
"alarmOriginatorEntityType", alarmOriginator.getEntityType().toString(),
"alarmOriginatorId", alarmOriginator.getId().toString()
);
}
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java

@ -35,7 +35,8 @@ public class Notification extends BaseData<NotificationId> {
private NotificationRequestId requestId; private NotificationRequestId requestId;
private UserId recipientId; private UserId recipientId;
private String type; // todo: maybe to enum private NotificationType type;
private String subject;
private String text; private String text;
private NotificationInfo info; private NotificationInfo info;

18
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationInfo.java

@ -15,20 +15,16 @@
*/ */
package org.thingsboard.server.common.data.notification; package org.thingsboard.server.common.data.notification;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.validation.NoXss; import java.util.Collections;
import java.util.Map;
@Data @Data
@ -36,6 +32,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "originatorType", defaultImpl = NotificationInfo.class) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "originatorType", defaultImpl = NotificationInfo.class)
@JsonSubTypes({ @JsonSubTypes({
@Type(name = "ALARM", value = AlarmOriginatedNotificationInfo.class), @Type(name = "ALARM", value = AlarmOriginatedNotificationInfo.class),
@Type(name = "RULE_NODE", value = RuleNodeOriginatedNotificationInfo.class)
}) })
public class NotificationInfo { public class NotificationInfo {
@ -46,4 +43,9 @@ public class NotificationInfo {
return null; return null;
} }
@JsonIgnore
public Map<String, String> getTemplateData() {
return Collections.emptyMap();
}
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationOriginatorType.java

@ -18,5 +18,6 @@ package org.thingsboard.server.common.data.notification;
public enum NotificationOriginatorType { public enum NotificationOriginatorType {
ADMIN, ADMIN,
ALARM, ALARM,
RULE_NODE RULE_NODE,
SYSTEM
} }

17
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java

@ -23,7 +23,6 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -32,12 +31,12 @@ import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.Map;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -67,15 +66,6 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
private NotificationRequestStatus status; private NotificationRequestStatus status;
private NotificationRequestStats stats; private NotificationRequestStats stats;
@JsonIgnore
private transient Map<String, String> templateContext;
@JsonIgnore
private transient HasCustomerId originatorEntity;
public void copyContext(NotificationRequest other) {
this.templateContext = other.getTemplateContext();
this.originatorEntity = other.getOriginatorEntity();
}
@JsonIgnore @JsonIgnore
@Override @Override
@ -83,4 +73,9 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
return "To targets " + targets + " via " + StringUtils.join(deliveryMethods, ", "); return "To targets " + targets + " via " + StringUtils.join(deliveryMethods, ", ");
} }
@JsonIgnore
public UserId getSenderId() {
return originatorType == NotificationOriginatorType.ADMIN ? (UserId) originatorEntityId : null;
}
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java

@ -22,15 +22,15 @@ import lombok.Data;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Data @Data
public class NotificationRequestStats { public class NotificationRequestStats {
private final Map<NotificationDeliveryMethod, AtomicInteger> sent; private final Map<NotificationDeliveryMethod, Set<String>> sent;
private final Map<NotificationDeliveryMethod, Map<String, String>> errors; private final Map<NotificationDeliveryMethod, Map<String, String>> errors;
@JsonIgnore @JsonIgnore
private final Map<NotificationDeliveryMethod, Set<UserId>> processedRecipients; private final Map<NotificationDeliveryMethod, Set<UserId>> processedRecipients;
@ -42,15 +42,15 @@ public class NotificationRequestStats {
} }
@JsonCreator @JsonCreator
public NotificationRequestStats(@JsonProperty("sent") Map<NotificationDeliveryMethod, AtomicInteger> sent, public NotificationRequestStats(@JsonProperty("sent") Map<NotificationDeliveryMethod, Set<String>> sent,
@JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors) { @JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors) {
this.sent = sent; this.sent = sent;
this.errors = errors; this.errors = errors;
this.processedRecipients = null; this.processedRecipients = Collections.emptyMap();
} }
public void reportSent(NotificationDeliveryMethod deliveryMethod, User recipient) { public void reportSent(NotificationDeliveryMethod deliveryMethod, User recipient) {
sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); sent.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getEmail());
processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId()); processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId());
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStatus.java

@ -16,6 +16,6 @@
package org.thingsboard.server.common.data.notification; package org.thingsboard.server.common.data.notification;
public enum NotificationRequestStatus { public enum NotificationRequestStatus {
PROCESSED, SENT,
SCHEDULED SCHEDULED
} }

24
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationEscalationConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -13,19 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.data.notification.rule; package org.thingsboard.server.common.data.notification;
import lombok.Data; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import javax.validation.Valid; @RequiredArgsConstructor
import javax.validation.constraints.NotNull; @Getter
import java.util.List; public enum NotificationType {
GENERIC("Generic"),
ADMIN("Admin"),
ALARM("Alarm"),
SCHEDULED_EVENT("Scheduled event"),
COMPLETED("Completed"),
FAILURE("Failure");
@Data private final String name;
public class NotificationEscalationConfig {
@NotNull
@Valid
private List<NonConfirmedNotificationEscalation> escalations;
} }

23
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/SingleUserNotificationTargetConfig.java → common/data/src/main/java/org/thingsboard/server/common/data/notification/RuleNodeOriginatedNotificationInfo.java

@ -13,22 +13,29 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.data.notification.targets; package org.thingsboard.server.common.data.notification;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.EntityId;
import javax.validation.constraints.NotNull; import java.util.Map;
import java.util.UUID;
@Data @Data
public class SingleUserNotificationTargetConfig implements NotificationTargetConfig { @EqualsAndHashCode(callSuper = true)
public class RuleNodeOriginatedNotificationInfo extends NotificationInfo {
@NotNull private EntityId msgOriginator;
private UUID userId; private Map<String, String> msgMetadata;
@Override @Override
public NotificationTargetConfigType getType() { public NotificationOriginatorType getOriginatorType() {
return NotificationTargetConfigType.SINGLE_USER; return NotificationOriginatorType.RULE_NODE;
}
@Override
public Map<String, String> getTemplateData() {
return msgMetadata;
} }
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NonConfirmedNotificationEscalation.java → common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationEscalation.java

@ -20,14 +20,16 @@ import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
@Data @Data
public class NonConfirmedNotificationEscalation { public class NotificationEscalation {
@Max(NotificationRequestConfig.MAX_SENDING_DELAY) @Max(NotificationRequestConfig.MAX_SENDING_DELAY)
private int delayInSec; private int delayInSec;
@NotNull @NotEmpty
private NotificationTargetId notificationTargetId; private List<NotificationTargetId> notificationTargets;
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java

@ -36,7 +36,6 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class NotificationRule extends BaseData<NotificationRuleId> implements HasTenantId, HasName { public class NotificationRule extends BaseData<NotificationRuleId> implements HasTenantId, HasName {
@NotNull
private TenantId tenantId; private TenantId tenantId;
@NotBlank @NotBlank
private String name; private String name;
@ -46,6 +45,6 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
private List<NotificationDeliveryMethod> deliveryMethods; private List<NotificationDeliveryMethod> deliveryMethods;
@NotNull @NotNull
@Valid @Valid
private NotificationRuleConfig configuration; private NotificationRuleConfig configuration; // todo: add pg_tgrm index (but index is 2.5x size of the column)
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java

@ -16,17 +16,17 @@
package org.thingsboard.server.common.data.notification.rule; package org.thingsboard.server.common.data.notification.rule;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List;
@Data @Data
public class NotificationRuleConfig { public class NotificationRuleConfig {
@NotNull @NotEmpty
private NotificationTargetId initialNotificationTargetId;
@Valid @Valid
private NotificationEscalationConfig escalationConfig; private List<NotificationEscalation> escalations;
} }

15
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/AccountNotificationSettings.java

@ -0,0 +1,15 @@
package org.thingsboard.server.common.data.notification.settings;
import lombok.Data;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import java.util.Set;
@Data
public class AccountNotificationSettings {
private UserId userId;
private Set<NotificationDeliveryMethod> allowedNotifications;
}

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationDeliveryMethodConfig.java

@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotNull;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method", include = JsonTypeInfo.As.EXISTING_PROPERTY, @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method", include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true, defaultImpl = NotificationDeliveryMethodConfig.class) visible = true, defaultImpl = NotificationDeliveryMethodConfig.class)
@ -32,6 +34,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
public class NotificationDeliveryMethodConfig { public class NotificationDeliveryMethodConfig {
private boolean enabled; private boolean enabled;
@NotNull
private NotificationDeliveryMethod method; private NotificationDeliveryMethod method;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/settings/NotificationSettings.java

@ -18,11 +18,13 @@ package org.thingsboard.server.common.data.notification.settings;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotNull;
import java.util.Map; import java.util.Map;
@Data @Data
public class NotificationSettings { public class NotificationSettings {
@NotNull
// location on the screen, shown notifications count, timings of displaying // location on the screen, shown notifications count, timings of displaying
private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs; private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs;

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java

@ -23,7 +23,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({ @JsonSubTypes({
@Type(value = SingleUserNotificationTargetConfig.class, name = "SINGLE_USER"),
@Type(value = UserListNotificationTargetConfig.class, name = "USER_LIST"), @Type(value = UserListNotificationTargetConfig.class, name = "USER_LIST"),
@Type(value = CustomerUsersNotificationTargetConfig.class, name = "CUSTOMER_USERS"), @Type(value = CustomerUsersNotificationTargetConfig.class, name = "CUSTOMER_USERS"),
@Type(value = AllUsersNotificationTargetConfig.class, name = "ALL_USERS") @Type(value = AllUsersNotificationTargetConfig.class, name = "ALL_USERS")

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfigType.java

@ -16,9 +16,7 @@
package org.thingsboard.server.common.data.notification.targets; package org.thingsboard.server.common.data.notification.targets;
public enum NotificationTargetConfigType { public enum NotificationTargetConfigType {
SINGLE_USER,
USER_LIST, USER_LIST,
CUSTOMER_USERS, CUSTOMER_USERS,
ALL_USERS ALL_USERS

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/EmailDeliveryMethodNotificationTemplate.java

@ -24,7 +24,6 @@ import javax.validation.constraints.NotBlank;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate {
@NotBlank
private String subject; private String subject;
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplate.java

@ -22,9 +22,11 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@Data @Data
@ -37,9 +39,10 @@ public class NotificationTemplate extends BaseData<NotificationTemplateId> imple
private String name; private String name;
@NoXss @NoXss
@NotNull @NotNull
private String notificationType; private NotificationType notificationType;
private String notificationSubject;
@Valid @Valid
@NotNull @NotNull
private NotificationTemplateConfig configuration; private NotificationTemplateConfig configuration; // TODO: add pg_tgrm index
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/NotificationTemplateConfig.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.AssertTrue; import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.Map; import java.util.Map;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackConversation.java

@ -24,7 +24,7 @@ public class SlackConversation {
private String name; private String name;
public enum Type { public enum Type {
USER, DIRECT,
PUBLIC_CHANNEL, PUBLIC_CHANNEL,
PRIVATE_CHANNEL PRIVATE_CHANNEL
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/template/SlackDeliveryMethodNotificationTemplate.java

@ -18,6 +18,8 @@ package org.thingsboard.server.common.data.notification.template;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.util.Map;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate { public class SlackDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate {

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -657,6 +657,7 @@ public class ModelConstants {
public static final String NOTIFICATION_REQUEST_ID_PROPERTY = "request_id"; public static final String NOTIFICATION_REQUEST_ID_PROPERTY = "request_id";
public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id"; public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id";
public static final String NOTIFICATION_TYPE_PROPERTY = "type"; public static final String NOTIFICATION_TYPE_PROPERTY = "type";
public static final String NOTIFICATION_SUBJECT_PROPERTY = "subject";
public static final String NOTIFICATION_TEXT_PROPERTY = "text"; public static final String NOTIFICATION_TEXT_PROPERTY = "text";
public static final String NOTIFICATION_ORIGINATOR_TYPE_PROPERTY = "originator_type"; public static final String NOTIFICATION_ORIGINATOR_TYPE_PROPERTY = "originator_type";
public static final String NOTIFICATION_STATUS_PROPERTY = "status"; public static final String NOTIFICATION_STATUS_PROPERTY = "status";
@ -681,6 +682,7 @@ public class ModelConstants {
public static final String NOTIFICATION_TEMPLATE_TABLE_NAME = "notification_template"; public static final String NOTIFICATION_TEMPLATE_TABLE_NAME = "notification_template";
public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type"; public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY = "notification_type";
public static final String NOTIFICATION_TEMPLATE_NOTIFICATION_SUBJECT_PROPERTY = "notification_subject";
public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration"; public static final String NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY = "configuration";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};

11
dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationInfo; import org.thingsboard.server.common.data.notification.NotificationInfo;
import org.thingsboard.server.common.data.notification.NotificationOriginatorType; import org.thingsboard.server.common.data.notification.NotificationOriginatorType;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType; import org.thingsboard.server.dao.util.mapping.JsonStringType;
@ -52,8 +53,12 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
@Column(name = ModelConstants.NOTIFICATION_RECIPIENT_ID_PROPERTY, nullable = false) @Column(name = ModelConstants.NOTIFICATION_RECIPIENT_ID_PROPERTY, nullable = false)
private UUID recipientId; private UUID recipientId;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false) @Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false)
private String type; private NotificationType type;
@Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY)
private String subject;
@Column(name = ModelConstants.NOTIFICATION_TEXT_PROPERTY, nullable = false) @Column(name = ModelConstants.NOTIFICATION_TEXT_PROPERTY, nullable = false)
private String text; private String text;
@ -78,6 +83,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
setRequestId(getUuid(notification.getRequestId())); setRequestId(getUuid(notification.getRequestId()));
setRecipientId(getUuid(notification.getRecipientId())); setRecipientId(getUuid(notification.getRecipientId()));
setType(notification.getType()); setType(notification.getType());
setSubject(notification.getSubject());
setText(notification.getText()); setText(notification.getText());
setInfo(toJson(notification.getInfo())); setInfo(toJson(notification.getInfo()));
setOriginatorType(notification.getOriginatorType()); setOriginatorType(notification.getOriginatorType());
@ -91,7 +97,8 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
notification.setCreatedTime(createdTime); notification.setCreatedTime(createdTime);
notification.setRequestId(getEntityId(requestId, NotificationRequestId::new)); notification.setRequestId(getEntityId(requestId, NotificationRequestId::new));
notification.setRecipientId(getEntityId(recipientId, UserId::new)); notification.setRecipientId(getEntityId(recipientId, UserId::new));
notification.setText(type); notification.setType(type);
notification.setSubject(subject);
notification.setText(text); notification.setText(text);
notification.setInfo(fromJson(info, NotificationInfo.class)); notification.setInfo(fromJson(info, NotificationInfo.class));
notification.setOriginatorType(originatorType); notification.setOriginatorType(originatorType);

11
dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationTemplateEntity.java

@ -22,6 +22,7 @@ import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; 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.NotificationTemplateConfig;
import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.BaseSqlEntity;
@ -30,6 +31,8 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table; import javax.persistence.Table;
import java.util.UUID; import java.util.UUID;
@ -46,8 +49,12 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
@Column(name = ModelConstants.NAME_PROPERTY, nullable = false) @Column(name = ModelConstants.NAME_PROPERTY, nullable = false)
private String name; private String name;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY, nullable = false) @Column(name = ModelConstants.NOTIFICATION_TEMPLATE_NOTIFICATION_TYPE_PROPERTY, nullable = false)
private String notificationType; private NotificationType notificationType;
@Column(name = ModelConstants.NOTIFICATION_TEMPLATE_NOTIFICATION_SUBJECT_PROPERTY)
private String notificationSubject;
@Type(type = "json") @Type(type = "json")
@Column(name = ModelConstants.NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY, nullable = false) @Column(name = ModelConstants.NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY, nullable = false)
@ -61,6 +68,7 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
setTenantId(getTenantUuid(notificationTemplate.getTenantId())); setTenantId(getTenantUuid(notificationTemplate.getTenantId()));
setName(notificationTemplate.getName()); setName(notificationTemplate.getName());
setNotificationType(notificationTemplate.getNotificationType()); setNotificationType(notificationTemplate.getNotificationType());
setNotificationSubject(notificationTemplate.getNotificationSubject());
setConfiguration(toJson(notificationTemplate.getConfiguration())); setConfiguration(toJson(notificationTemplate.getConfiguration()));
} }
@ -72,6 +80,7 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
notificationTemplate.setTenantId(getTenantId(tenantId)); notificationTemplate.setTenantId(getTenantId(tenantId));
notificationTemplate.setName(name); notificationTemplate.setName(name);
notificationTemplate.setNotificationType(notificationType); notificationTemplate.setNotificationType(notificationType);
notificationTemplate.setNotificationSubject(notificationSubject);
notificationTemplate.setConfiguration(fromJson(configuration, NotificationTemplateConfig.class)); notificationTemplate.setConfiguration(fromJson(configuration, NotificationTemplateConfig.class));
return notificationTemplate; return notificationTemplate;
} }

4
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java

@ -43,9 +43,7 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
@Override @Override
public NotificationRequest saveNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) { public NotificationRequest saveNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) {
notificationRequestValidator.validate(notificationRequest, NotificationRequest::getTenantId); notificationRequestValidator.validate(notificationRequest, NotificationRequest::getTenantId);
NotificationRequest savedNotificationRequest = notificationRequestDao.save(tenantId, notificationRequest); return notificationRequestDao.save(tenantId, notificationRequest);
savedNotificationRequest.copyContext(notificationRequest);
return savedNotificationRequest;
} }
@Override @Override

7
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java

@ -19,11 +19,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationInfo;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -73,4 +71,9 @@ public class DefaultNotificationService implements NotificationService {
return notificationDao.countUnreadByUserId(tenantId, userId); return notificationDao.countUnreadByUserId(tenantId, userId);
} }
@Override
public boolean deleteNotification(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationDao.deleteByIdAndUserId(tenantId, userId, notificationId);
}
} }

12
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -20,12 +20,10 @@ import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.util.Map; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
@Service @Service
@ -55,13 +53,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), NotificationSettings.class)) .map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), NotificationSettings.class))
.orElseGet(() -> { .orElseGet(() -> {
NotificationSettings settings = new NotificationSettings(); NotificationSettings settings = new NotificationSettings();
NotificationDeliveryMethodConfig defaultConfig = new NotificationDeliveryMethodConfig(); settings.setDeliveryMethodsConfigs(Collections.emptyMap());
defaultConfig.setEnabled(true);
settings.setDeliveryMethodsConfigs(Map.of(
NotificationDeliveryMethod.WEBSOCKET, defaultConfig,
NotificationDeliveryMethod.EMAIL, defaultConfig,
NotificationDeliveryMethod.SMS, defaultConfig
));
return settings; return settings;
}); });
} }

6
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
import org.thingsboard.server.common.data.notification.targets.CustomerUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.CustomerUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; 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.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.SingleUserNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -73,11 +72,6 @@ public class DefaultNotificationTargetService implements NotificationTargetServi
@Override @Override
public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, CustomerId customerId, NotificationTargetConfig targetConfig, PageLink pageLink) { public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, CustomerId customerId, NotificationTargetConfig targetConfig, PageLink pageLink) {
switch (targetConfig.getType()) { switch (targetConfig.getType()) {
case SINGLE_USER: {
UserId userId = new UserId(((SingleUserNotificationTargetConfig) targetConfig).getUserId());
User user = userService.findUserById(tenantId, userId);
return new PageData<>(List.of(user), 1, 1, false);
}
case USER_LIST: { case USER_LIST: {
List<User> users = ((UserListNotificationTargetConfig) targetConfig).getUsersIds().stream() List<User> users = ((UserListNotificationTargetConfig) targetConfig).getUsersIds().stream()
.map(UserId::new).map(userId -> userService.findUserById(tenantId, userId)) .map(UserId::new).map(userId -> userService.findUserById(tenantId, userId))

2
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java

@ -37,4 +37,6 @@ public interface NotificationDao extends Dao<Notification> {
PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink); PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink);
boolean deleteByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId);
} }

5
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -88,6 +88,11 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
return DaoUtil.toPageData(notificationRepository.findByRequestId(notificationRequestId.getId(), DaoUtil.toPageable(pageLink))); return DaoUtil.toPageData(notificationRepository.findByRequestId(notificationRequestId.getId(), DaoUtil.toPageable(pageLink)));
} }
@Override
public boolean deleteByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationRepository.deleteByIdAndRecipientId(notificationId.getId(), userId.getId()) != 0;
}
@Override @Override
protected Class<NotificationEntity> getEntityClass() { protected Class<NotificationEntity> getEntityClass() {
return NotificationEntity.class; return NotificationEntity.class;

2
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java

@ -48,4 +48,6 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
Page<NotificationEntity> findByRequestId(UUID requestId, Pageable pageable); Page<NotificationEntity> findByRequestId(UUID requestId, Pageable pageable);
int deleteByIdAndRecipientId(UUID id, UUID recipientId);
} }

12
dao/src/main/resources/sql/schema-entities.sql

@ -793,7 +793,8 @@ CREATE TABLE IF NOT EXISTS notification_template (
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE, tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
notification_type VARCHAR(255) NOT NULL, notification_type VARCHAR(32) NOT NULL,
notification_subject VARCHAR(255),
configuration VARCHAR(10000) NOT NULL configuration VARCHAR(10000) NOT NULL
); );
@ -811,7 +812,7 @@ CREATE TABLE IF NOT EXISTS notification_request (
id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY, id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_request_tenant_id REFERENCES tenant(id) ON DELETE CASCADE, tenant_id UUID NULL CONSTRAINT fk_notification_request_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
targets VARCHAR(255) NOT NULL, targets VARCHAR(5000) NOT NULL,
template_id UUID NOT NULL, template_id UUID NOT NULL,
info VARCHAR(1000), info VARCHAR(1000),
delivery_methods VARCHAR(255), delivery_methods VARCHAR(255),
@ -821,15 +822,16 @@ CREATE TABLE IF NOT EXISTS notification_request (
originator_entity_type VARCHAR(32), originator_entity_type VARCHAR(32),
rule_id UUID NULL, rule_id UUID NULL,
status VARCHAR(32), status VARCHAR(32),
stats VARCHAR(1000) stats VARCHAR(10000)
); );
CREATE TABLE IF NOT EXISTS notification ( CREATE TABLE IF NOT EXISTS notification (
id UUID NOT NULL, id UUID NOT NULL,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
request_id UUID NOT NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE, 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, recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
type VARCHAR(255) NOT NULL, type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL, text VARCHAR(1000) NOT NULL,
info VARCHAR(1000), info VARCHAR(1000),
originator_type VARCHAR(32) NOT NULL, originator_type VARCHAR(32) NOT NULL,

7
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationManager.java → rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

@ -20,8 +20,9 @@ import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationType;
public interface NotificationManager { public interface NotificationCenter {
NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest); NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest);
@ -29,7 +30,11 @@ public interface NotificationManager {
NotificationRequest updateNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest); NotificationRequest updateNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest);
void sendBasicNotification(TenantId tenantId, UserId recipientId, NotificationType type, String subject, String text);
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId);
} }

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -280,7 +280,7 @@ public interface TbContext {
SmsSenderFactory getSmsSenderFactory(); SmsSenderFactory getSmsSenderFactory();
NotificationManager getNotificationManager(); NotificationCenter getNotificationCenter();
SlackService getSlackService(); SlackService getSlackService();

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

@ -24,6 +24,8 @@ import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.notification.NotificationOriginatorType; import org.thingsboard.server.common.data.notification.NotificationOriginatorType;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.RuleNodeOriginatedNotificationInfo;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -49,18 +51,23 @@ public class TbNotificationNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
RuleNodeOriginatedNotificationInfo notificationInfo = new RuleNodeOriginatedNotificationInfo();
notificationInfo.setMsgOriginator(msg.getOriginator());
notificationInfo.setMsgMetadata(msg.getMetaData().getData());
NotificationRequest notificationRequest = NotificationRequest.builder() NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(ctx.getTenantId()) .tenantId(ctx.getTenantId())
.targets(config.getTargets()) .targets(config.getTargets())
.templateId(config.getTemplateId()) .templateId(config.getTemplateId())
.info(notificationInfo)
.deliveryMethods(config.getDeliveryMethods()) .deliveryMethods(config.getDeliveryMethods())
.additionalConfig(new NotificationRequestConfig())
.originatorType(NotificationOriginatorType.RULE_NODE) .originatorType(NotificationOriginatorType.RULE_NODE)
.originatorEntityId(ctx.getSelfId()) // todo: duplicate originator from msg originator, set originator's customerId .originatorEntityId(ctx.getSelfId())
.build(); .build();
notificationRequest.setTemplateContext(msg.getMetaData().getData());
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> { DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> {
return ctx.getNotificationManager().processNotificationRequest(ctx.getTenantId(), notificationRequest); return ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest);
}), }),
r -> { r -> {
TbMsgMetaData msgMetaData = msg.getMetaData().copy(); TbMsgMetaData msgMetaData = msg.getMetaData().copy();

Loading…
Cancel
Save