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,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
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
);
@ -46,7 +47,7 @@ CREATE TABLE IF NOT EXISTS notification_request (
id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID 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,
info VARCHAR(1000),
delivery_methods VARCHAR(255),
@ -56,16 +57,17 @@ CREATE TABLE IF NOT EXISTS notification_request (
originator_entity_type VARCHAR(32),
rule_id UUID NULL,
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 TABLE IF NOT EXISTS notification (
id UUID 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,
type VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
info VARCHAR(1000),
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.stereotype.Component;
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.slack.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -327,7 +327,7 @@ public class ActorSystemContext {
@Autowired
@Getter
private NotificationManager notificationManager;
private NotificationCenter notificationCenter;
@Autowired
@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.ListeningExecutor;
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.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -687,8 +687,8 @@ class DefaultTbContext implements TbContext {
}
@Override
public NotificationManager getNotificationManager() {
return mainCtx.getNotificationManager();
public NotificationCenter getNotificationCenter() {
return mainCtx.getNotificationCenter();
}
@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.RequestParam;
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.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationId;
@ -60,7 +60,7 @@ public class NotificationController extends BaseController {
private final NotificationService notificationService;
private final NotificationRequestService notificationRequestService;
private final NotificationManager notificationManager;
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
@GetMapping("/notifications")
@ -76,15 +76,21 @@ public class NotificationController extends BaseController {
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')")
public void markNotificationAsRead(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@ -93,11 +99,11 @@ public class NotificationController extends BaseController {
if (notificationRequest.getId() != null) {
throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it");
}
notificationRequest.setTenantId(user.getTenantId());
checkEntity(notificationRequest.getId(), notificationRequest, Resource.NOTIFICATION_REQUEST);
notificationRequest.setOriginatorType(NotificationOriginatorType.ADMIN);
notificationRequest.setOriginatorEntityId(user.getId());
notificationRequest.setOriginatorEntity(user);
if (notificationRequest.getInfo() != null && notificationRequest.getInfo().getOriginatorType() != null) {
throw new IllegalArgumentException("Unsupported notification info type");
}
@ -105,7 +111,7 @@ public class NotificationController extends BaseController {
notificationRequest.setStatus(null);
notificationRequest.setStats(null);
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationManager::processNotificationRequest);
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::processNotificationRequest);
}
@GetMapping("/notification/request/{id}")
@ -132,13 +138,13 @@ public class NotificationController extends BaseController {
public void deleteNotificationRequest(@PathVariable UUID id) throws Exception {
NotificationRequestId notificationRequestId = new NotificationRequestId(id);
NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE);
doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationManager::deleteNotificationRequest);
doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::deleteNotificationRequest);
}
@PostMapping("/notification/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationSettings saveNotificationSettings(@RequestBody NotificationSettings notificationSettings,
public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings,
@AuthenticationPrincipal SecurityUser user) {
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId();
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")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule) throws Exception {
notificationRule.setTenantId(getTenantId());
checkEntity(notificationRule.getId(), notificationRule, Resource.NOTIFICATION_RULE);
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')")
public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget,
@AuthenticationPrincipal SecurityUser user) throws Exception {
notificationTarget.setTenantId(user.getTenantId());
checkEntity(notificationTarget.getId(), notificationTarget, Resource.NOTIFICATION_TARGET);
if (!user.isSystemAdmin()) {
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")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception {
notificationTemplate.setTenantId(getTenantId());
checkEntity(notificationTemplate.getId(), notificationTemplate, Resource.NOTIFICATION_TEMPLATE);
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.stereotype.Service;
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.server.common.data.User;
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.Notification;
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.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
@ -66,13 +70,14 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationManager extends AbstractSubscriptionService implements NotificationManager, NotificationChannel {
public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel {
private final NotificationTargetService notificationTargetService;
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());
notificationRequest.setTenantId(tenantId);
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);
NotificationTemplate notificationTemplate = notificationTemplateService.findNotificationTemplateById(tenantId, notificationRequest.getTemplateId());
notificationRequest.getDeliveryMethods().forEach(deliveryMethod -> {
if (!settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod) || !settings.getDeliveryMethodsConfigs().get(deliveryMethod).isEnabled()) {
throw new IllegalArgumentException("Delivery method " + deliveryMethod + " is not enabled or configured");
if (settings.getDeliveryMethodsConfigs().containsKey(deliveryMethod) &&
!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);
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId)
.request(savedNotificationRequest)
.settings(settings)
.template(notificationTemplate)
.build();
ctx.init(notificationTemplateService);
ctx.init();
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationTargetId targetId : notificationRequest.getTargets()) {
DaoUtil.processBatches(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getOriginatorCustomerId(), targetId, pageLink);
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getCustomerId(), targetId, pageLink);
}, 200, recipientsBatch -> {
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationDeliveryMethod deliveryMethod : savedNotificationRequest.getDeliveryMethods()) {
NotificationChannel notificationChannel = channels.get(deliveryMethod);
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);
}
}
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;
}
@ -186,6 +218,7 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationTemplate().getNotificationType())
.subject(ctx.getNotificationTemplate().getNotificationSubject())
.text(text)
.info(request.getInfo())
.originatorType(request.getOriginatorType())
@ -197,7 +230,31 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
log.error("Failed to create notification for recipient {}", recipient.getId(), 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
@ -205,8 +262,25 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) {
log.debug("Marking notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
onNotificationUpdate(tenantId, recipientId, notification, false);
NotificationUpdate update = NotificationUpdate.builder()
.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;
}
private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew) {
NotificationUpdate update = NotificationUpdate.builder()
.notification(notification)
.isNew(isNew)
.build();
private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
log.trace("Submitting notification update for recipient {}: {}", recipientId, update);
return Futures.submit(() -> {
forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> {
@ -270,7 +340,7 @@ public class DefaultNotificationManager extends AbstractSubscriptionService impl
}
@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.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.event.EventListener;
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.alarm.Alarm;
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.NotificationRequestConfig;
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.NotificationRuleConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -58,7 +58,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
private final NotificationRuleService notificationRuleService;
private final NotificationRequestService notificationRequestService;
@Autowired @Lazy
private NotificationManager notificationManager;
private NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
@Override
@ -72,9 +72,15 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
}
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(() -> {
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;
});
}
@ -91,20 +97,16 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
}
for (NotificationRequest notificationRequest : notificationRequests) {
if (notificationRequest.getStatus() == NotificationRequestStatus.SCHEDULED) {
notificationManager.deleteNotificationRequest(tenantId, notificationRequest.getId());
notificationCenter.deleteNotificationRequest(tenantId, notificationRequest.getId());
}
}
}
if (notificationRequests.isEmpty()) {
NotificationRuleConfig config = notificationRule.getConfiguration();
NotificationTargetId initialNotificationTargetId = config.getInitialNotificationTargetId();
if (initialNotificationTargetId != null) {
submitNotificationRequest(tenantId, initialNotificationTargetId, notificationRule, alarm, 0);
}
if (config.getEscalationConfig() != null) {
for (NonConfirmedNotificationEscalation escalation : config.getEscalationConfig().getEscalations()) {
submitNotificationRequest(tenantId, escalation.getNotificationTargetId(), notificationRule, alarm, escalation.getDelayInSec());
for (NotificationEscalation escalation : config.getEscalations()) {
for (NotificationTargetId targetId : escalation.getNotificationTargets()) {
submitNotificationRequest(tenantId, targetId, notificationRule, alarm, escalation.getDelayInSec());
}
}
} else {
@ -113,7 +115,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
NotificationInfo previousNotificationInfo = notificationRequest.getInfo();
if (!previousNotificationInfo.equals(newNotificationInfo)) {
notificationRequest.setInfo(newNotificationInfo);
notificationManager.updateNotificationRequest(tenantId, notificationRequest);
notificationCenter.updateNotificationRequest(tenantId, notificationRequest);
}
}
}
@ -129,12 +131,6 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
config.setSendingDelayInSec(delayInSec);
}
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()
.tenantId(tenantId)
.targets(List.of(targetId))
@ -145,10 +141,8 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
.ruleId(notificationRule.getId())
.originatorType(NotificationOriginatorType.ALARM)
.originatorEntityId(alarm.getId())
.originatorEntity(alarm)
.templateContext(templateContext)
.build();
notificationManager.processNotificationRequest(tenantId, notificationRequest);
notificationCenter.processNotificationRequest(tenantId, notificationRequest);
}
private NotificationInfo constructNotificationInfo(Alarm alarm) {
@ -159,6 +153,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
.alarmOriginator(alarm.getOriginator())
.alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus())
.customerId(alarm.getCustomerId())
.build();
}
@ -173,7 +168,7 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId();
List<NotificationRequestId> scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId);
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 org.springframework.context.event.EventListener;
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.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -56,7 +58,7 @@ import java.util.concurrent.TimeUnit;
@SuppressWarnings("UnstableApiUsage")
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService {
private final NotificationManager notificationManager;
private final NotificationCenter notificationCenter;
private final NotificationRequestService notificationRequestService;
private final SchedulerComponent scheduler;
private final NotificationExecutorService notificationExecutor;
@ -107,7 +109,16 @@ public class DefaultNotificationSchedulerService extends AbstractPartitionBasedS
if (notificationRequest == null) return;
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());
}, 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.Getter;
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.id.CustomerId;
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.NotificationOriginatorType;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
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.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import java.util.HashMap;
import java.util.Map;
@ -41,38 +41,35 @@ public class NotificationProcessingContext {
@Getter
private final TenantId tenantId;
private final HasCustomerId originatorEntity;
private final NotificationSettings settings;
@Getter
private final NotificationRequest request;
private final Map<String, String> additionalTemplateContext;
@Getter
private NotificationTemplate notificationTemplate;
private final NotificationTemplate notificationTemplate;
private Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates;
@Getter
private final NotificationRequestStats stats;
@Builder
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings) {
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings,
NotificationTemplate template) {
this.tenantId = tenantId;
this.originatorEntity = request.getOriginatorEntity();
this.request = request;
this.settings = settings;
this.additionalTemplateContext = request.getTemplateContext();
this.notificationTemplate = template;
this.stats = new NotificationRequestStats();
}
public void init(NotificationTemplateService templateService) {
notificationTemplate = templateService.findNotificationTemplateById(tenantId, request.getTemplateId());
NotificationTemplateConfig config = notificationTemplate.getConfiguration();
public void init() {
NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration();
templates = templateConfig.getTemplates();
for (NotificationDeliveryMethod deliveryMethod : request.getDeliveryMethods()) {
DeliveryMethodNotificationTemplate template = config.getTemplates().get(deliveryMethod);
DeliveryMethodNotificationTemplate template = templates.get(deliveryMethod);
if (StringUtils.isEmpty(template.getBody())) {
template.setBody(config.getDefaultTextTemplate());
template.setBody(templateConfig.getDefaultTextTemplate());
}
}
templates = config.getTemplates();
}
public <T extends DeliveryMethodNotificationTemplate> T getTemplate(NotificationDeliveryMethod deliveryMethod) {
@ -88,14 +85,20 @@ public class NotificationProcessingContext {
templateContext.put("email", recipient.getEmail());
templateContext.put("firstName", Strings.nullToEmpty(recipient.getFirstName()));
templateContext.put("lastName", Strings.nullToEmpty(recipient.getLastName()));
if (additionalTemplateContext != null) {
templateContext.putAll(additionalTemplateContext);
if (request.getInfo() != null) {
templateContext.putAll(request.getInfo().getTemplateData());
}
return templateContext;
}
public CustomerId getOriginatorCustomerId() {
return originatorEntity != null ? originatorEntity.getCustomerId() : null;
public CustomerId getCustomerId() {
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 lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
@ -35,8 +36,16 @@ public class EmailNotificationChannel implements NotificationChannel {
@Override
public ListenableFuture<Void> sendNotification(User recipient, String text, NotificationProcessingContext ctx) {
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(() -> {
mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), text, template.getSubject());
mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), mailSubject, text);
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 org.apache.commons.lang3.StringUtils;
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.server.common.data.User;
import org.thingsboard.server.common.data.notification.AlreadySentException;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
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.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@ -56,7 +56,7 @@ public class SlackNotificationChannel implements NotificationChannel {
if (StringUtils.isNoneEmpty(recipient.getFirstName(), recipient.getLastName())) {
String username = StringUtils.join(new String[]{recipient.getFirstName(), recipient.getLastName()}, ' ');
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) {
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
public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversation.Type conversationType) {
return cache.get(conversationType + ":" + token, k -> {
if (conversationType == SlackConversation.Type.USER) {
if (conversationType == SlackConversation.Type.DIRECT) {
UsersListRequest request = UsersListRequest.builder()
.limit(CONVERSATIONS_LIMIT)
.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.context.annotation.Lazy;
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.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -58,7 +58,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private final NotificationService notificationService;
private final TbLocalSubscriptionService localSubscriptionService;
private final NotificationManager notificationManager;
private final NotificationCenter notificationCenter;
private final TbServiceInfoProvider serviceInfoProvider;
@Autowired @Lazy
private WebSocketService wsService;
@ -130,29 +130,45 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification();
if (update.isNew()) {
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
subscription.getTotalUnreadCounter().incrementAndGet();
if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) {
Set<UUID> beyondLimit = subscription.getSortedNotifications().stream().skip(subscription.getLimit())
.map(IdBased::getUuidId).collect(Collectors.toSet());
beyondLimit.forEach(notificationId -> subscription.getLatestUnreadNotifications().remove(notificationId));
UUID notificationId = update.getNotificationId().getId();
switch (update.getUpdateType()) {
case CREATED: {
subscription.getLatestUnreadNotifications().put(notificationId, notification);
subscription.getTotalUnreadCounter().incrementAndGet();
if (subscription.getLatestUnreadNotifications().size() > subscription.getLimit()) {
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));
} else {
if (notification.getStatus() != NotificationStatus.READ) {
if (subscription.getLatestUnreadNotifications().containsKey(notification.getUuidId())) {
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
case UPDATED: {
if (update.getUpdatedStatus() == NotificationStatus.READ) {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} else {
subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
}
} else if (notification.getStatus() != NotificationStatus.READ) {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
subscription.getLatestUnreadNotifications().put(notificationId, notification);
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
}
}
} else {
if (subscription.getLatestUnreadNotifications().containsKey(notification.getUuidId())) {
break;
}
case DELETED: {
if (subscription.getLatestUnreadNotifications().containsKey(notificationId)) {
fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} else {
} else if (notification.getStatus() != NotificationStatus.READ) {
subscription.getTotalUnreadCounter().decrementAndGet();
sendUpdate(subscription.getSessionId(), subscription.createCountUpdate());
}
break;
}
}
}
@ -162,13 +178,13 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
NotificationRequestId notificationRequestId = update.getNotificationRequestId();
if (update.isDeleted()) {
if (subscription.getLatestUnreadNotifications().values().stream()
.anyMatch(notification -> notification.getRequestId().equals(notificationRequestId))) {
.anyMatch(notification -> notificationRequestId.equals(notification.getRequestId()))) {
fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
}
} else {
subscription.getLatestUnreadNotifications().values().stream()
.filter(notification -> notification.getRequestId().equals(notificationRequestId))
.filter(notification -> notificationRequestId.equals(notification.getRequestId()))
.forEach(notification -> {
notification.setInfo(update.getNotificationInfo());
sendUpdate(subscription.getSessionId(), subscription.createPartialUpdate(notification));
@ -189,13 +205,28 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void handleNotificationUpdate(NotificationsCountSubscription subscription, NotificationUpdate update) {
log.trace("[{}, subId: {}] Handling notification update for count sub: {}", subscription.getSessionId(), subscription.getSubscriptionId(), update);
Notification notification = update.getNotification();
if (update.isNew()) {
subscription.getUnreadCounter().incrementAndGet();
} else if (notification.getStatus() == NotificationStatus.READ) {
// for now this can only happen when user marks notification as read
subscription.getUnreadCounter().decrementAndGet();
switch (update.getUpdateType()) {
case CREATED: {
System.err.println("NotificationsCountSubscription CREATED");
subscription.getUnreadCounter().incrementAndGet();
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) {
@ -219,7 +250,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
cmd.getNotifications().stream()
.map(NotificationId::new)
.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
});
}

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.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NotificationUpdate {
private NotificationId notificationId;
private NotificationStatus updatedStatus;
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.web.context.WebApplicationContext;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -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.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.tenant.TenantProfileService;
@ -111,6 +115,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
@Slf4j
public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
public static final int TIMEOUT = 30;
protected ObjectMapper mapper = new ObjectMapper();
@ -483,6 +488,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return protoTransportPayloadConfiguration;
}
protected Device createDevice(String deviceName, String type, String accessToken) throws Exception {
Device device = new Device();
device.setName(deviceName);
device.setType(type);
DeviceCredentials credentials = new DeviceCredentials();
credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
credentials.setCredentialsId(accessToken);
SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(device, credentials);
return doPost("/api/device-with-credentials", request, Device.class);
}
protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
@ -584,7 +601,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
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 {
return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) {
@ -694,10 +711,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
public class IdComparator<D extends HasId> implements Comparator<D> {
@Override
public int compare(D o1, D o2) {
return o1.getId().getId().compareTo(o2.getId().getId());
}
}
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;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.data.Offset;
import org.java_websocket.client.WebSocketClient;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.rule.engine.api.NotificationManager;
import org.thingsboard.rule.engine.api.NotificationCenter;
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.NotificationRequestStats;
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.SingleUserNotificationTargetConfig;
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.controller.AbstractControllerTest;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -59,228 +46,252 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
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
@Slf4j
public class NotificationApiTest extends AbstractControllerTest {
public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationManager notificationManager;
private NotificationCenter notificationCenter;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
connectWsClient();
}
@Test
public void testSubscribingToUnreadNotificationsCount() {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1);
String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().subscribeForUnreadNotificationsCount();
getWsClient().waitForReply(true);
wsClient.subscribeForUnreadNotificationsCount();
wsClient.waitForReply(true);
UnreadNotificationsCountUpdate update = getWsClient().getLastCountUpdate();
UnreadNotificationsCountUpdate update = wsClient.getLastCountUpdate();
assertThat(update.getTotalUnreadCount()).isEqualTo(2);
}
@Test
public void testReceivingCountUpdates_multipleSessions() {
getWsClient().subscribeForUnreadNotificationsCount();
getAnotherWsClient().subscribeForUnreadNotificationsCount();
getWsClient().waitForReply(true);
getAnotherWsClient().waitForReply(true);
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
public void testReceivingCountUpdates_multipleSessions() throws Exception {
connectOtherWsClient();
wsClient.subscribeForUnreadNotificationsCount();
otherWsClient.subscribeForUnreadNotificationsCount();
wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isZero();
wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Notification";
submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true);
wsClient.waitForUpdate(true);
otherWsClient.waitForUpdate(true);
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
assertThat(wsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
}
@Test
public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1);
String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().subscribeForUnreadNotifications(10);
getAnotherWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
getAnotherWsClient().waitForReply(true);
connectOtherWsClient();
wsClient.subscribeForUnreadNotifications(10);
otherWsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText1, notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText1, notificationText2);
checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText1, notificationText2);
checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText1, notificationText2);
}
@Test
public void testReceivingNotificationUpdates_multipleSessions() {
getWsClient().subscribeForUnreadNotifications(10);
getAnotherWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
getAnotherWsClient().waitForReply(true);
UnreadNotificationsUpdate notificationsUpdate = getWsClient().getLastDataUpdate();
public void testReceivingNotificationUpdates_multipleSessions() throws Exception {
connectOtherWsClient();
wsClient.subscribeForUnreadNotifications(10);
otherWsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
UnreadNotificationsUpdate notificationsUpdate = wsClient.getLastDataUpdate();
assertThat(notificationsUpdate.getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true);
wsClient.waitForUpdate(true);
otherWsClient.waitForUpdate(true);
checkPartialNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText, 1);
checkPartialNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText, 1);
checkPartialNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText, 1);
checkPartialNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText, 1);
}
@Test
public void testMarkingAsRead_multipleSessions() {
getWsClient().subscribeForUnreadNotifications(10);
getAnotherWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
getAnotherWsClient().waitForReply(true);
getAnotherWsClient().subscribeForUnreadNotificationsCount();
getAnotherWsClient().waitForReply(true);
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2);
public void testMarkingAsRead_multipleSessions() throws Exception {
connectOtherWsClient();
wsClient.subscribeForUnreadNotifications(10);
otherWsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
otherWsClient.waitForReply(true);
otherWsClient.subscribeForUnreadNotificationsCount();
otherWsClient.waitForReply(true);
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate(2);
String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), notificationText1);
getWsClient().waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true);
Notification notification1 = getWsClient().getLastDataUpdate().getUpdate();
wsClient.waitForUpdate(true);
otherWsClient.waitForUpdate(true);
Notification notification1 = wsClient.getLastDataUpdate().getUpdate();
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2);
wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate(2);
String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), notificationText2);
getWsClient().waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true);
assertThat(getWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(2);
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2);
getWsClient().markNotificationAsRead(notification1.getUuidId());
getWsClient().waitForUpdate(true);
getAnotherWsClient().waitForUpdate(true);
checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
wsClient.waitForUpdate(true);
otherWsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(otherWsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isEqualTo(2);
wsClient.registerWaitForUpdate();
otherWsClient.registerWaitForUpdate(2);
wsClient.markNotificationAsRead(notification1.getUuidId());
wsClient.waitForUpdate(true);
otherWsClient.waitForUpdate(true);
checkFullNotificationsUpdate(wsClient.getLastDataUpdate(), notificationText2);
checkFullNotificationsUpdate(otherWsClient.getLastDataUpdate(), notificationText2);
assertThat(otherWsClient.getLastCountUpdate().getTotalUnreadCount()).isOne();
}
@Test
public void testDelayedNotificationRequest() throws Exception {
getWsClient().subscribeForUnreadNotifications(5);
getWsClient().waitForReply(true);
wsClient.subscribeForUnreadNotifications(5);
wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Was scheduled for 5 sec";
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText, 5);
assertThat(notificationRequest.getStatus()).isEqualTo(NotificationRequestStatus.SCHEDULED);
await().atLeast(4, TimeUnit.SECONDS)
.atMost(6, TimeUnit.SECONDS)
.until(() -> 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.getCreatedTime() - notificationRequest.getCreatedTime())
.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
public void whenNotificationRequestIsDeleted_thenDeleteNotifications() throws Exception {
getWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
wsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test");
getWsClient().waitForUpdate(true);
assertThat(getWsClient().getNotifications()).singleElement().extracting(Notification::getRequestId)
wsClient.waitForUpdate(true);
assertThat(wsClient.getNotifications()).singleElement().extracting(Notification::getRequestId)
.isEqualTo(notificationRequest.getId());
assertThat(getWsClient().getUnreadCount()).isOne();
assertThat(wsClient.getUnreadCount()).isOne();
getWsClient().registerWaitForUpdate();
wsClient.registerWaitForUpdate();
deleteNotificationRequest(notificationRequest.getId());
getWsClient().waitForUpdate(true);
wsClient.waitForUpdate(true);
assertThat(getWsClient().getNotifications()).isEmpty();
assertThat(getWsClient().getUnreadCount()).isZero();
assertThat(wsClient.getNotifications()).isEmpty();
assertThat(wsClient.getUnreadCount()).isZero();
loginCustomerUser();
assertThat(getMyNotifications(false, 10)).size().isZero();
}
@Test
public void whenNotificationRequestIsUpdated_thenUpdateNotifications() throws Exception {
getWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
wsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
String notificationText = "Text";
getWsClient().registerWaitForUpdate();
wsClient.registerWaitForUpdate();
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), notificationText);
getWsClient().waitForUpdate(true);
Notification initialNotification = getWsClient().getLastDataUpdate().getUpdate();
wsClient.waitForUpdate(true);
Notification initialNotification = wsClient.getLastDataUpdate().getUpdate();
loginCustomerUser();
assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(initialNotification);
assertThat(initialNotification.getInfo()).isNotNull().isEqualTo(notificationRequest.getInfo());
getWsClient().registerWaitForUpdate();
wsClient.registerWaitForUpdate();
NotificationInfo newNotificationInfo = new NotificationInfo();
newNotificationInfo.setDescription("New description");
notificationRequest.setInfo(newNotificationInfo);
notificationManager.updateNotificationRequest(tenantId, notificationRequest);
getWsClient().waitForUpdate(true);
Notification updatedNotification = getWsClient().getLastDataUpdate().getUpdate();
notificationCenter.updateNotificationRequest(tenantId, notificationRequest);
wsClient.waitForUpdate(true);
Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(updatedNotification.getInfo()).isEqualTo(newNotificationInfo);
assertThat(getMyNotifications(false, 10)).singleElement().isEqualTo(updatedNotification);
}
@Test
public void testNotificationUpdatesForALotOfUsers() throws Exception {
int usersCount = 100;
Map<User, NotificationApiWsClient> wsSessions = createUsersAndSetUpWsSessions(usersCount);
wsSessions.values().forEach(wsClient -> {
int usersCount = 100; // FIXME: sometimes if set e.g. to 150, up to 5 WS sessions don't receive update
Map<User, NotificationApiWsClient> sessions = new HashMap<>();
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.waitForReply(true);
wsClient.subscribeForUnreadNotificationsCount();
wsClient.waitForReply(true);
});
}
sessions.values().forEach(wsClient -> wsClient.waitForUpdate(true));
loginTenantAdmin();
NotificationTarget notificationTarget = new NotificationTarget();
UserListNotificationTargetConfig config = new UserListNotificationTargetConfig();
config.setUsersIds(wsSessions.keySet().stream().map(User::getUuidId).collect(Collectors.toList()));
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);
sessions.forEach((user, wsClient) -> wsClient.registerWaitForUpdate(2));
NotificationRequest notificationRequest = submitNotificationRequest(targets, "Hello, ${email}", 0,
NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL);
await().atMost(20, TimeUnit.SECONDS)
.pollDelay(1, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> wsSessions.values().stream()
.allMatch(wsClient -> wsClient.getLastDataUpdate() != null
&& wsClient.getLastCountUpdate() != null));
wsSessions.forEach((user, wsClient) -> {
.until(() -> {
long receivedUpdate = sessions.values().stream()
.filter(wsClient -> wsClient.getLastDataUpdate() != null
&& wsClient.getLastCountUpdate() != null)
.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.getLastCountUpdate().getTotalUnreadCount()).isOne();
@ -293,148 +304,56 @@ public class NotificationApiTest extends AbstractControllerTest {
await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null);
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 {
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;
sessions.values().forEach(WebSocketClient::close);
}
@Test
public void testNotificationRequestStats() throws Exception {
getWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(true);
wsClient.subscribeForUnreadNotifications(10);
wsClient.waitForReply(true);
getWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
wsClient.registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(customerUserId);
NotificationRequest notificationRequest = submitNotificationRequest(notificationTarget.getId(), "Test :)",
NotificationDeliveryMethod.WEBSOCKET, NotificationDeliveryMethod.EMAIL, NotificationDeliveryMethod.SMS);
getWsClient().waitForUpdate();
wsClient.waitForUpdate();
await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null);
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats();
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET)).hasValue(1);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(1);
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEBSOCKET)).containsOnly(CUSTOMER_USER_EMAIL);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).containsOnly(CUSTOMER_USER_EMAIL);
assertThat(stats.getErrors().get(NotificationDeliveryMethod.SMS)).size().isOne();
System.err.println(stats);
}
private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) {
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications);
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE);
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getSubject).containsOnly(DEFAULT_NOTIFICATION_SUBJECT);
assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedNotifications.length);
}
private void checkPartialNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String expectedNotification, int expectedUnreadCount) {
assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getText).isEqualTo(expectedNotification);
assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getType).isEqualTo(DEFAULT_NOTIFICATION_TYPE);
assertThat(notificationsUpdate.getUpdate()).extracting(Notification::getSubject).isEqualTo(DEFAULT_NOTIFICATION_SUBJECT);
assertThat(notificationsUpdate.getTotalUnreadCount()).isEqualTo(expectedUnreadCount);
}
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.CustomerUsersNotificationTargetConfig;
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.page.PageData;
import org.thingsboard.server.controller.AbstractControllerTest;
@ -62,14 +61,6 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
.contains("name 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();
userListConfig.setUsersIds(Collections.emptyList());
notificationTarget.setConfiguration(userListConfig);
@ -85,18 +76,13 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
NotificationTarget notificationTarget = new NotificationTarget();
notificationTarget.setTenantId(differentTenantId);
notificationTarget.setName("Target 1");
UserListNotificationTargetConfig userListConfig = new UserListNotificationTargetConfig();
userListConfig.setUsersIds(List.of(customerUserId.getId(), tenantAdminUserId.getId()));
notificationTarget.setConfiguration(userListConfig);
saveAndGetError(notificationTarget, status().isForbidden());
SingleUserNotificationTargetConfig singleUserConfig = new SingleUserNotificationTargetConfig();
singleUserConfig.setUserId(customerUserId.getId());
notificationTarget.setConfiguration(singleUserConfig);
saveAndGetError(notificationTarget, status().isForbidden());
loginSysAdmin();
notificationTarget.setTenantId(TenantId.SYS_TENANT_ID);
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.setName(null);
notificationTemplate.setNotificationType(null);
notificationTemplate.setNotificationSubject(null);
notificationTemplate.setConfiguration(null);
String validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
@ -68,7 +69,6 @@ public class NotificationTemplateApiTest extends AbstractControllerTest {
validationError = saveAndGetError(notificationTemplate, status().isBadRequest());
assertThat(validationError)
.doesNotContain("defaultTextTemplate must be specified")
.contains("subject must not be")
.contains("name is malformed");
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);
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);
case NOTIFICATION_TEMPLATE:
return new NotificationTemplateId(uuid);
case NOTIFICATION:
return new NotificationId(uuid);
}
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.AlarmStatus;
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 java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@ -37,10 +40,21 @@ public class AlarmOriginatedNotificationInfo extends NotificationInfo {
private EntityId alarmOriginator;
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private CustomerId customerId;
@Override
public NotificationOriginatorType getOriginatorType() {
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 UserId recipientId;
private String type; // todo: maybe to enum
private NotificationType type;
private String subject;
private String text;
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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
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.EntityId;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Collections;
import java.util.Map;
@Data
@ -36,6 +32,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "originatorType", defaultImpl = NotificationInfo.class)
@JsonSubTypes({
@Type(name = "ALARM", value = AlarmOriginatedNotificationInfo.class),
@Type(name = "RULE_NODE", value = RuleNodeOriginatedNotificationInfo.class)
})
public class NotificationInfo {
@ -46,4 +43,9 @@ public class NotificationInfo {
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 {
ADMIN,
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 org.apache.commons.lang3.StringUtils;
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.HasTenantId;
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.NotificationTemplateId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
@ -67,15 +66,6 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
private NotificationRequestStatus status;
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
@Override
@ -83,4 +73,9 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
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.id.UserId;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Data
public class NotificationRequestStats {
private final Map<NotificationDeliveryMethod, AtomicInteger> sent;
private final Map<NotificationDeliveryMethod, Set<String>> sent;
private final Map<NotificationDeliveryMethod, Map<String, String>> errors;
@JsonIgnore
private final Map<NotificationDeliveryMethod, Set<UserId>> processedRecipients;
@ -42,15 +42,15 @@ public class NotificationRequestStats {
}
@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) {
this.sent = sent;
this.errors = errors;
this.processedRecipients = null;
this.processedRecipients = Collections.emptyMap();
}
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());
}

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;
public enum NotificationRequestStatus {
PROCESSED,
SENT,
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
* 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;
import javax.validation.constraints.NotNull;
import java.util.List;
@RequiredArgsConstructor
@Getter
public enum NotificationType {
GENERIC("Generic"),
ADMIN("Admin"),
ALARM("Alarm"),
SCHEDULED_EVENT("Scheduled event"),
COMPLETED("Completed"),
FAILURE("Failure");
@Data
public class NotificationEscalationConfig {
@NotNull
@Valid
private List<NonConfirmedNotificationEscalation> escalations;
private final String name;
}

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
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.targets;
package org.thingsboard.server.common.data.notification;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.EntityId;
import javax.validation.constraints.NotNull;
import java.util.UUID;
import java.util.Map;
@Data
public class SingleUserNotificationTargetConfig implements NotificationTargetConfig {
@EqualsAndHashCode(callSuper = true)
public class RuleNodeOriginatedNotificationInfo extends NotificationInfo {
@NotNull
private UUID userId;
private EntityId msgOriginator;
private Map<String, String> msgMetadata;
@Override
public NotificationTargetConfigType getType() {
return NotificationTargetConfigType.SINGLE_USER;
public NotificationOriginatorType getOriginatorType() {
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 javax.validation.constraints.Max;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
public class NonConfirmedNotificationEscalation {
public class NotificationEscalation {
@Max(NotificationRequestConfig.MAX_SENDING_DELAY)
private int delayInSec;
@NotNull
private NotificationTargetId notificationTargetId;
@NotEmpty
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)
public class NotificationRule extends BaseData<NotificationRuleId> implements HasTenantId, HasName {
@NotNull
private TenantId tenantId;
@NotBlank
private String name;
@ -46,6 +45,6 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
private List<NotificationDeliveryMethod> deliveryMethods;
@NotNull
@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;
import lombok.Data;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
public class NotificationRuleConfig {
@NotNull
private NotificationTargetId initialNotificationTargetId;
@NotEmpty
@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 org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotNull;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "method", include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true, defaultImpl = NotificationDeliveryMethodConfig.class)
@ -32,6 +34,7 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
public class NotificationDeliveryMethodConfig {
private boolean enabled;
@NotNull
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 org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.constraints.NotNull;
import java.util.Map;
@Data
public class NotificationSettings {
@NotNull
// location on the screen, shown notifications count, timings of displaying
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)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@Type(value = SingleUserNotificationTargetConfig.class, name = "SINGLE_USER"),
@Type(value = UserListNotificationTargetConfig.class, name = "USER_LIST"),
@Type(value = CustomerUsersNotificationTargetConfig.class, name = "CUSTOMER_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;
public enum NotificationTargetConfigType {
SINGLE_USER,
USER_LIST,
CUSTOMER_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)
public class EmailDeliveryMethodNotificationTemplate extends DeliveryMethodNotificationTemplate {
@NotBlank
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.id.NotificationTemplateId;
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 javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
@ -37,9 +39,10 @@ public class NotificationTemplate extends BaseData<NotificationTemplateId> imple
private String name;
@NoXss
@NotNull
private String notificationType;
private NotificationType notificationType;
private String notificationSubject;
@Valid
@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.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
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;
public enum Type {
USER,
DIRECT,
PUBLIC_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.EqualsAndHashCode;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
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_RECIPIENT_ID_PROPERTY = "recipient_id";
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_ORIGINATOR_TYPE_PROPERTY = "originator_type";
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_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";
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.NotificationOriginatorType;
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.ModelConstants;
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)
private UUID recipientId;
@Enumerated(EnumType.STRING)
@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)
private String text;
@ -78,6 +83,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
setRequestId(getUuid(notification.getRequestId()));
setRecipientId(getUuid(notification.getRecipientId()));
setType(notification.getType());
setSubject(notification.getSubject());
setText(notification.getText());
setInfo(toJson(notification.getInfo()));
setOriginatorType(notification.getOriginatorType());
@ -91,7 +97,8 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
notification.setCreatedTime(createdTime);
notification.setRequestId(getEntityId(requestId, NotificationRequestId::new));
notification.setRecipientId(getEntityId(recipientId, UserId::new));
notification.setText(type);
notification.setType(type);
notification.setSubject(subject);
notification.setText(text);
notification.setInfo(fromJson(info, NotificationInfo.class));
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.thingsboard.server.common.data.id.NotificationTemplateId;
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.NotificationTemplateConfig;
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.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;
import java.util.UUID;
@ -46,8 +49,12 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
@Column(name = ModelConstants.NAME_PROPERTY, nullable = false)
private String name;
@Enumerated(EnumType.STRING)
@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")
@Column(name = ModelConstants.NOTIFICATION_TEMPLATE_CONFIGURATION_PROPERTY, nullable = false)
@ -61,6 +68,7 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
setTenantId(getTenantUuid(notificationTemplate.getTenantId()));
setName(notificationTemplate.getName());
setNotificationType(notificationTemplate.getNotificationType());
setNotificationSubject(notificationTemplate.getNotificationSubject());
setConfiguration(toJson(notificationTemplate.getConfiguration()));
}
@ -72,6 +80,7 @@ public class NotificationTemplateEntity extends BaseSqlEntity<NotificationTempla
notificationTemplate.setTenantId(getTenantId(tenantId));
notificationTemplate.setName(name);
notificationTemplate.setNotificationType(notificationType);
notificationTemplate.setNotificationSubject(notificationSubject);
notificationTemplate.setConfiguration(fromJson(configuration, NotificationTemplateConfig.class));
return notificationTemplate;
}

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

@ -43,9 +43,7 @@ public class DefaultNotificationRequestService implements NotificationRequestSer
@Override
public NotificationRequest saveNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) {
notificationRequestValidator.validate(notificationRequest, NotificationRequest::getTenantId);
NotificationRequest savedNotificationRequest = notificationRequestDao.save(tenantId, notificationRequest);
savedNotificationRequest.copyContext(notificationRequest);
return savedNotificationRequest;
return notificationRequestDao.save(tenantId, notificationRequest);
}
@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 org.springframework.stereotype.Service;
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.UserId;
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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -73,4 +71,9 @@ public class DefaultNotificationService implements NotificationService {
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.server.common.data.AdminSettings;
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.dao.settings.AdminSettingsService;
import java.util.Map;
import java.util.Collections;
import java.util.Optional;
@Service
@ -55,13 +53,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), NotificationSettings.class))
.orElseGet(() -> {
NotificationSettings settings = new NotificationSettings();
NotificationDeliveryMethodConfig defaultConfig = new NotificationDeliveryMethodConfig();
defaultConfig.setEnabled(true);
settings.setDeliveryMethodsConfigs(Map.of(
NotificationDeliveryMethod.WEBSOCKET, defaultConfig,
NotificationDeliveryMethod.EMAIL, defaultConfig,
NotificationDeliveryMethod.SMS, defaultConfig
));
settings.setDeliveryMethodsConfigs(Collections.emptyMap());
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.NotificationTarget;
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.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -73,11 +72,6 @@ public class DefaultNotificationTargetService implements NotificationTargetServi
@Override
public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, CustomerId customerId, NotificationTargetConfig targetConfig, PageLink pageLink) {
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: {
List<User> users = ((UserListNotificationTargetConfig) targetConfig).getUsersIds().stream()
.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);
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)));
}
@Override
public boolean deleteByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId) {
return notificationRepository.deleteByIdAndRecipientId(notificationId.getId(), userId.getId()) != 0;
}
@Override
protected Class<NotificationEntity> getEntityClass() {
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);
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,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
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
);
@ -811,7 +812,7 @@ CREATE TABLE IF NOT EXISTS notification_request (
id UUID NOT NULL CONSTRAINT notification_request_pkey PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID 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,
info VARCHAR(1000),
delivery_methods VARCHAR(255),
@ -821,15 +822,16 @@ CREATE TABLE IF NOT EXISTS notification_request (
originator_entity_type VARCHAR(32),
rule_id UUID NULL,
status VARCHAR(32),
stats VARCHAR(1000)
stats VARCHAR(10000)
);
CREATE TABLE IF NOT EXISTS notification (
id UUID 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,
type VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
info VARCHAR(1000),
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.UserId;
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);
@ -29,7 +30,11 @@ public interface NotificationManager {
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 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();
NotificationManager getNotificationManager();
NotificationCenter getNotificationCenter();
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.server.common.data.notification.NotificationOriginatorType;
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.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -49,18 +51,23 @@ public class TbNotificationNode implements TbNode {
@Override
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()
.tenantId(ctx.getTenantId())
.targets(config.getTargets())
.templateId(config.getTemplateId())
.info(notificationInfo)
.deliveryMethods(config.getDeliveryMethods())
.additionalConfig(new NotificationRequestConfig())
.originatorType(NotificationOriginatorType.RULE_NODE)
.originatorEntityId(ctx.getSelfId()) // todo: duplicate originator from msg originator, set originator's customerId
.originatorEntityId(ctx.getSelfId())
.build();
notificationRequest.setTemplateContext(msg.getMetaData().getData());
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> {
return ctx.getNotificationManager().processNotificationRequest(ctx.getTenantId(), notificationRequest);
return ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest);
}),
r -> {
TbMsgMetaData msgMetaData = msg.getMetaData().copy();

Loading…
Cancel
Save