Browse Source

Notifications api permission checks, WS improvements, new target configs

pull/7511/head
ViacheslavKlimov 4 years ago
parent
commit
3734a0da49
  1. 1
      application/src/main/data/upgrade/3.4.2/schema_update.sql
  2. 18
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 27
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  4. 76
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  5. 3
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  6. 44
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationProcessingService.java
  7. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  11. 9
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java
  15. 22
      application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java
  16. 193
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultWebSocketService.java
  17. 20
      application/src/main/java/org/thingsboard/server/service/ws/WsCmd.java
  18. 77
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationAsReadCmd.java
  21. 10
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java
  22. 29
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java
  23. 4
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java
  24. 7
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java
  25. 44
      application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java
  26. 47
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java
  27. 11
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java
  28. 1
      application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java
  29. 3
      application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java
  30. 1
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java
  31. 50
      application/src/test/java/org/thingsboard/server/service/notification/NotificationsWebSocketClient.java
  32. 67
      application/src/test/java/org/thingsboard/server/service/notification/NotificationsWsApiTest.java
  33. 11
      common/cluster-api/src/main/proto/queue.proto
  34. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java
  35. 9
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java
  36. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  37. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequest.java
  38. 28
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/AllUsersNotificationTargetConfig.java
  39. 32
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/CustomerUsersNotificationTargetConfig.java
  40. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfig.java
  41. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/NotificationTargetConfigType.java
  42. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/SingleUserNotificationTargetConfig.java
  43. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/UserListNotificationTargetConfig.java
  44. 6
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  45. 15
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  46. 53
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  47. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  48. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  49. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  50. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java
  51. 3
      dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java
  52. 5
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  53. 2
      dao/src/main/resources/sql/schema-entities-idx.sql

1
application/src/main/data/upgrade/3.4.2/schema_update.sql

@ -49,5 +49,6 @@ CREATE TABLE IF NOT EXISTS notification (
severity VARCHAR(32), severity VARCHAR(32),
status VARCHAR(32) status VARCHAR(32)
) PARTITION BY RANGE (created_time); ) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_created_time ON notification(recipient_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_status_and_created_time ON notification(recipient_id, status, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_status_and_created_time ON notification(recipient_id, status, created_time DESC);

18
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
@ -58,6 +59,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
@ -75,6 +77,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.RpcId;
@ -130,8 +133,8 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.ota.OtaPackageStateService;
@ -284,6 +287,9 @@ public abstract class BaseController {
@Autowired @Autowired
protected TbNotificationEntityService notificationEntityService; protected TbNotificationEntityService notificationEntityService;
@Autowired
protected EntityActionService entityActionService;
@Autowired @Autowired
protected QueueService queueService; protected QueueService queueService;
@ -912,6 +918,16 @@ public abstract class BaseController {
return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null; return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
} }
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E savedEntity, ActionType actionType) {
logEntityAction(user, entityType, null, savedEntity, actionType, null);
}
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) { protected void sendEntityNotificationMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType action) {
sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action); sendNotificationMsgToEdge(tenantId, null, entityId, null, null, action);
} }

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

@ -28,6 +28,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -81,9 +83,15 @@ public class NotificationController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationRequest createNotificationRequest(@RequestBody NotificationRequest notificationRequest, public NotificationRequest createNotificationRequest(@RequestBody NotificationRequest notificationRequest,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.NOTIFICATION, Operation.CREATE); accessControlService.checkPermission(user, Resource.NOTIFICATION_REQUEST, Operation.CREATE, null, notificationRequest);
// read permission for target's users try {
return notificationProcessingService.processNotificationRequest(user.getTenantId(), notificationRequest); NotificationRequest savedNotificationRequest = notificationProcessingService.processNotificationRequest(user.getTenantId(), notificationRequest);
logEntityAction(user, EntityType.NOTIFICATION_REQUEST, savedNotificationRequest, ActionType.ADDED);
return savedNotificationRequest;
} catch (Exception e) {
logEntityAction(user, EntityType.NOTIFICATION_REQUEST, notificationRequest, null, ActionType.ADDED, e);
throw e;
}
} }
@GetMapping("/notification/request/{id}") @GetMapping("/notification/request/{id}")
@ -102,16 +110,23 @@ public class NotificationController extends BaseController {
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
accessControlService.checkPermission(user, Resource.NOTIFICATION, Operation.CREATE);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationService.findNotificationRequestsByTenantIdAndPageLink(user.getTenantId(), pageLink); return notificationService.findNotificationRequestsByTenantIdAndPageLink(user.getTenantId(), pageLink);
} }
@DeleteMapping("/notification/request/{id}") @DeleteMapping("/notification/request/{id}")
public void deleteNotificationRequest(@PathVariable UUID id, public void deleteNotificationRequest(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationRequestId notificationRequestId = new NotificationRequestId(id); NotificationRequestId notificationRequestId = new NotificationRequestId(id);
notificationProcessingService.deleteNotificationRequest(user.getTenantId(), notificationRequestId); NotificationRequest notificationRequest = notificationService.findNotificationRequestById(user.getTenantId(), notificationRequestId);
accessControlService.checkPermission(user, Resource.NOTIFICATION_REQUEST, Operation.DELETE, notificationRequestId, notificationRequest);
try {
notificationProcessingService.deleteNotificationRequest(user.getTenantId(), notificationRequestId);
logEntityAction(user, EntityType.NOTIFICATION_REQUEST, notificationRequest, ActionType.DELETED);
} catch (Exception e) {
logEntityAction(user, EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationRequest, ActionType.DELETED, e);
throw e;
}
} }
} }

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

@ -27,24 +27,26 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfigType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@ -52,37 +54,59 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class NotificationTargetController extends BaseController { public class NotificationTargetController extends BaseController {
// fixme: permission check, log action, events
private final NotificationTargetService notificationTargetService; private final NotificationTargetService notificationTargetService;
private final UserService userService;
@PostMapping("/target") @PostMapping("/target")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget saveNotificationTarget(@RequestBody NotificationTarget notificationTarget, public NotificationTarget saveNotificationTarget(@RequestBody NotificationTarget notificationTarget,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// fixme: read permission check for users in target accessControlService.checkPermission(user, Resource.NOTIFICATION_TARGET, Operation.CREATE, null, notificationTarget);
return notificationTargetService.saveNotificationTarget(user.getTenantId(), notificationTarget); if (!user.isSystemAdmin()) {
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration();
if (targetConfig.getType() == NotificationTargetConfigType.SINGLE_USER ||
targetConfig.getType() == NotificationTargetConfigType.USER_LIST) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), notificationTarget.getConfiguration(), null);
for (User recipient : recipients.getData()) {
accessControlService.checkPermission(user, Resource.USER, Operation.READ, recipient.getId(), recipient);
}
}
}
try {
NotificationTarget savedNotificationTarget = notificationTargetService.saveNotificationTarget(user.getTenantId(), notificationTarget);
logEntityAction(user, EntityType.NOTIFICATION_TARGET, savedNotificationTarget,
notificationTarget.getId() == null ? ActionType.ADDED : ActionType.UPDATED);
return savedNotificationTarget;
} catch (Exception e) {
logEntityAction(user, EntityType.NOTIFICATION_TARGET, notificationTarget, null,
notificationTarget.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw e;
}
} }
@GetMapping("/target/{id}") @GetMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public NotificationTarget getNotificationTargetById(@PathVariable UUID id, public NotificationTarget getNotificationTargetById(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationTargetId notificationTargetId = new NotificationTargetId(id); NotificationTargetId notificationTargetId = new NotificationTargetId(id);
return notificationTargetService.findNotificationTargetById(user.getTenantId(), notificationTargetId); NotificationTarget notificationTarget = notificationTargetService.findNotificationTargetById(user.getTenantId(), notificationTargetId);
accessControlService.checkPermission(user, Resource.NOTIFICATION_TARGET, Operation.READ, notificationTargetId, notificationTarget);
return notificationTarget;
} }
@GetMapping("/target/{id}/recipients") @PostMapping("/target/recipients")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<User> getRecipientsForNotificationTarget(@PathVariable UUID id, public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @RequestParam int pageSize,
NotificationTargetId notificationTargetId = new NotificationTargetId(id); @RequestParam int page,
// fixme: to page data @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// todo: check read permission for recipients PageLink pageLink = createPageLink(pageSize, page, null, null, null);
List<User> recipients = new ArrayList<>(); PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), notificationTarget.getConfiguration(), pageLink);
for (UserId userId : notificationTargetService.findRecipientsForNotificationTarget(user.getTenantId(), notificationTargetId)) { if (!user.isSystemAdmin()) {
recipients.add(checkUserId(userId, Operation.READ)); for (User recipient : recipients.getData()) {
accessControlService.checkPermission(user, Resource.USER, Operation.READ, recipient.getId(), recipient);
}
} }
return recipients; return recipients;
} }
@ -102,9 +126,17 @@ public class NotificationTargetController extends BaseController {
@DeleteMapping("/target/{id}") @DeleteMapping("/target/{id}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void deleteNotificationTarget(@PathVariable UUID id, public void deleteNotificationTarget(@PathVariable UUID id,
@AuthenticationPrincipal SecurityUser user) { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
NotificationTargetId notificationTargetId = new NotificationTargetId(id); NotificationTargetId notificationTargetId = new NotificationTargetId(id);
notificationTargetService.deleteNotificationTarget(user.getTenantId(), notificationTargetId); NotificationTarget notificationTarget = checkNotNull(notificationTargetService.findNotificationTargetById(user.getTenantId(), notificationTargetId));
accessControlService.checkPermission(user, Resource.NOTIFICATION_TARGET, Operation.DELETE, notificationTargetId, notificationTarget);
try {
notificationTargetService.deleteNotificationTarget(user.getTenantId(), notificationTargetId);
logEntityAction(user, EntityType.NOTIFICATION_TARGET, notificationTarget, ActionType.DELETED);
} catch (Exception e) {
logEntityAction(user, EntityType.NOTIFICATION_TARGET, null, notificationTarget, ActionType.DELETED, e);
}
} }
} }

3
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
@ -69,7 +70,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>(); private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>();
@Autowired @Autowired @Lazy
private WebSocketService webSocketService; private WebSocketService webSocketService;
@Autowired @Autowired

44
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationProcessingService.java

@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.NotificationProcessingService; import org.thingsboard.server.dao.notification.NotificationProcessingService;
import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.NotificationsTopicService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
@ -43,9 +43,7 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import org.thingsboard.server.service.telemetry.AbstractSubscriptionService; import org.thingsboard.server.service.telemetry.AbstractSubscriptionService;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -56,32 +54,23 @@ public class DefaultNotificationProcessingService extends AbstractSubscriptionSe
private final NotificationTargetService notificationTargetService; private final NotificationTargetService notificationTargetService;
private final NotificationService notificationService; private final NotificationService notificationService;
private final UserService userService;
private final DbCallbackExecutorService dbCallbackExecutorService; private final DbCallbackExecutorService dbCallbackExecutorService;
private final NotificationsTopicService notificationsTopicService; private final NotificationsTopicService notificationsTopicService;
public DefaultNotificationProcessingService(TbClusterService clusterService, PartitionService partitionService, public DefaultNotificationProcessingService(TbClusterService clusterService, PartitionService partitionService,
NotificationTargetService notificationTargetService, NotificationTargetService notificationTargetService,
NotificationService notificationService, UserService userService, NotificationService notificationService,
DbCallbackExecutorService dbCallbackExecutorService, DbCallbackExecutorService dbCallbackExecutorService,
NotificationsTopicService notificationsTopicService) { NotificationsTopicService notificationsTopicService) {
super(clusterService, partitionService); super(clusterService, partitionService);
this.notificationTargetService = notificationTargetService; this.notificationTargetService = notificationTargetService;
this.notificationService = notificationService; this.notificationService = notificationService;
this.userService = userService;
this.dbCallbackExecutorService = dbCallbackExecutorService; this.dbCallbackExecutorService = dbCallbackExecutorService;
this.notificationsTopicService = notificationsTopicService; this.notificationsTopicService = notificationsTopicService;
} }
@Override @Override
public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) { public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) {
List<UserId> recipientsIds = notificationTargetService.findRecipientsForNotificationTarget(tenantId, notificationRequest.getTargetId());
List<User> recipients = new ArrayList<>();
for (UserId recipientId : recipientsIds) {
User recipient = userService.findUserById(tenantId, recipientId); // todo: add caching
recipients.add(recipient);
}
notificationRequest.setTenantId(tenantId); notificationRequest.setTenantId(tenantId);
NotificationRequest savedNotificationRequest = notificationService.createNotificationRequest(tenantId, notificationRequest); NotificationRequest savedNotificationRequest = notificationService.createNotificationRequest(tenantId, notificationRequest);
@ -90,20 +79,31 @@ public class DefaultNotificationProcessingService extends AbstractSubscriptionSe
// todo: delayed sending; check all delayed notification requests on start up, schedule send // todo: delayed sending; check all delayed notification requests on start up, schedule send
} }
for (User recipient : recipients) { DaoUtil.processBatches(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, notificationRequest.getTargetId(), pageLink);
}, 100, recipients -> {
dbCallbackExecutorService.submit(() -> { dbCallbackExecutorService.submit(() -> {
Notification notification = createNotification(recipient, savedNotificationRequest); for (User recipient : recipients) {
onNotificationUpdate(recipient.getTenantId(), recipient.getId(), notification); try {
Notification notification = createNotification(recipient, savedNotificationRequest);
onNotificationUpdate(recipient.getTenantId(), recipient.getId(), notification, true);
} catch (Exception e) {
log.error("Failed to create notification for recipient {}", recipient.getId(), e);
}
}
}); });
} });
return savedNotificationRequest; return savedNotificationRequest;
} }
@Override @Override
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) { public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.updateNotificationStatus(tenantId, notificationId, NotificationStatus.READ); boolean updated = notificationService.updateNotificationStatus(tenantId, recipientId, notificationId, NotificationStatus.READ);
onNotificationUpdate(tenantId, recipientId, notification); if (updated) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
onNotificationUpdate(tenantId, recipientId, notification, false);
}
} }
@Override @Override
@ -134,11 +134,11 @@ public class DefaultNotificationProcessingService extends AbstractSubscriptionSe
return TbNodeUtils.processTemplate(template, context); return TbNodeUtils.processTemplate(template, context);
} }
private void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification) { private void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew) {
forwardToSubscriptionManagerServiceOrSendToCore(tenantId, recipientId, subscriptionManagerService -> { forwardToSubscriptionManagerServiceOrSendToCore(tenantId, recipientId, subscriptionManagerService -> {
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, notification, TbCallback.EMPTY); subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, notification, isNew, TbCallback.EMPTY);
}, () -> { }, () -> {
return TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, notification); return TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, notification, isNew);
}); });
} }

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -464,6 +464,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TransportProtos.NotificationsSubscriptionUpdateProto notificationsSubUpdateProto = msg.getNotificationsSubUpdate(); TransportProtos.NotificationsSubscriptionUpdateProto notificationsSubUpdateProto = msg.getNotificationsSubUpdate();
NotificationsSubscriptionUpdate notificationsSubscriptionUpdate = NotificationsSubscriptionUpdate.builder() NotificationsSubscriptionUpdate notificationsSubscriptionUpdate = NotificationsSubscriptionUpdate.builder()
.notification(JacksonUtil.fromString(notificationsSubUpdateProto.getNotification(), Notification.class)) .notification(JacksonUtil.fromString(notificationsSubUpdateProto.getNotification(), Notification.class))
.isNewNotification(notificationsSubUpdateProto.getIsNewNotification())
.build(); .build();
localSubscriptionService.onSubscriptionUpdate(notificationsSubUpdateProto.getSessionId(), localSubscriptionService.onSubscriptionUpdate(notificationsSubUpdateProto.getSessionId(),
notificationsSubUpdateProto.getSubscriptionId(), notificationsSubscriptionUpdate, callback); notificationsSubUpdateProto.getSubscriptionId(), notificationsSubscriptionUpdate, callback);
@ -481,6 +482,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getAlarmSub()), callback); subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getAlarmSub()), callback);
} else if (msg.hasNotificationsSub()) { } else if (msg.hasNotificationsSub()) {
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getNotificationsSub()), callback); subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getNotificationsSub()), callback);
} else if (msg.hasNotificationsCountSub()) {
subscriptionManagerService.addSubscription(TbSubscriptionUtils.fromProto(msg.getNotificationsCountSub()), callback);
} else if (msg.hasSubClose()) { } else if (msg.hasSubClose()) {
TbSubscriptionCloseProto closeProto = msg.getSubClose(); TbSubscriptionCloseProto closeProto = msg.getSubClose();
subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback); subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback);
@ -525,7 +528,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TenantId tenantId = TenantId.fromUUID(new UUID(notificationUpdateProto.getTenantIdMSB(), notificationUpdateProto.getTenantIdLSB())); TenantId tenantId = TenantId.fromUUID(new UUID(notificationUpdateProto.getTenantIdMSB(), notificationUpdateProto.getTenantIdLSB()));
UserId recipientId = new UserId(new UUID(notificationUpdateProto.getRecipientIdMSB(), notificationUpdateProto.getRecipientIdLSB())); UserId recipientId = new UserId(new UUID(notificationUpdateProto.getRecipientIdMSB(), notificationUpdateProto.getRecipientIdLSB()));
Notification notification = JacksonUtil.fromString(notificationUpdateProto.getNotification(), Notification.class); Notification notification = JacksonUtil.fromString(notificationUpdateProto.getNotification(), Notification.class);
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, notification, callback); boolean isNew = notificationUpdateProto.getIsNew();
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, notification, isNew, callback);
} else if (msg.hasNotificationRequestDelete()) { } else if (msg.hasNotificationRequestDelete()) {
TransportProtos.NotificationRequestDeleteProto notificationRequestDeleteProto = msg.getNotificationRequestDelete(); TransportProtos.NotificationRequestDeleteProto notificationRequestDeleteProto = msg.getNotificationRequestDelete();
TenantId tenantId = TenantId.fromUUID(new UUID(notificationRequestDeleteProto.getTenantIdMSB(), notificationRequestDeleteProto.getTenantIdLSB())); TenantId tenantId = TenantId.fromUUID(new UUID(notificationRequestDeleteProto.getTenantIdMSB(), notificationRequestDeleteProto.getTenantIdLSB()));

3
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -44,7 +44,8 @@ public enum Resource {
RPC(EntityType.RPC), RPC(EntityType.RPC),
QUEUE(EntityType.QUEUE), QUEUE(EntityType.QUEUE),
VERSION_CONTROL, VERSION_CONTROL,
NOTIFICATION; NOTIFICATION_TARGET(EntityType.NOTIFICATION_TARGET),
NOTIFICATION_REQUEST(EntityType.NOTIFICATION_REQUEST);
private final EntityType entityType; private final EntityType entityType;

2
application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java

@ -40,6 +40,8 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);
put(Resource.TB_RESOURCE, systemEntityPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker);
put(Resource.QUEUE, systemEntityPermissionChecker); put(Resource.QUEUE, systemEntityPermissionChecker);
put(Resource.NOTIFICATION_TARGET, systemEntityPermissionChecker);
put(Resource.NOTIFICATION_REQUEST, systemEntityPermissionChecker);
} }
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {

3
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -49,7 +49,8 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.RPC, tenantEntityPermissionChecker); put(Resource.RPC, tenantEntityPermissionChecker);
put(Resource.QUEUE, queuePermissionChecker); put(Resource.QUEUE, queuePermissionChecker);
put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker); put(Resource.VERSION_CONTROL, PermissionChecker.allowAllPermissionChecker);
put(Resource.NOTIFICATION, PermissionChecker.allowAllPermissionChecker); put(Resource.NOTIFICATION_TARGET, tenantEntityPermissionChecker);
put(Resource.NOTIFICATION_REQUEST, tenantEntityPermissionChecker);
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

9
application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java

@ -330,14 +330,16 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
} }
@Override @Override
public void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, TbCallback callback) { public void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew, TbCallback callback) {
Set<TbSubscription> subscriptions = subscriptionsByEntityId.get(recipientId); Set<TbSubscription> subscriptions = subscriptionsByEntityId.get(recipientId);
if (subscriptions != null) { if (subscriptions != null) {
NotificationsSubscriptionUpdate subscriptionUpdate = NotificationsSubscriptionUpdate.builder() NotificationsSubscriptionUpdate subscriptionUpdate = NotificationsSubscriptionUpdate.builder()
.notification(notification) .notification(notification)
.isNewNotification(isNew)
.build(); .build();
subscriptions.stream() subscriptions.stream()
.filter(subscription -> subscription.getType() == TbSubscriptionType.NOTIFICATIONS) .filter(subscription -> subscription.getType() == TbSubscriptionType.NOTIFICATIONS
|| subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)
.forEach(subscription -> { .forEach(subscription -> {
if (serviceId.equals(subscription.getServiceId())) { if (serviceId.equals(subscription.getServiceId())) {
localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(), localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(),
@ -362,7 +364,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene
subscriptionsByEntityId.entrySet().stream() subscriptionsByEntityId.entrySet().stream()
.filter(subEntry -> subEntry.getKey().getEntityType() == EntityType.USER) .filter(subEntry -> subEntry.getKey().getEntityType() == EntityType.USER)
.flatMap(subEntry -> subEntry.getValue().stream() .flatMap(subEntry -> subEntry.getValue().stream()
.filter(sub -> sub.getType() == TbSubscriptionType.NOTIFICATIONS) .filter(sub -> sub.getType() == TbSubscriptionType.NOTIFICATIONS
|| sub.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)
.filter(sub -> sub.getServiceId().equals(serviceId))) .filter(sub -> sub.getServiceId().equals(serviceId)))
.forEach(subscription -> { .forEach(subscription -> {
localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, TbCallback.EMPTY); localSubscriptionService.onSubscriptionUpdate(subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, TbCallback.EMPTY);

3
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java

@ -172,7 +172,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
@Override @Override
public void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback) { public void onSubscriptionUpdate(String sessionId, int subscriptionId, NotificationsSubscriptionUpdate update, TbCallback callback) {
TbSubscription subscription = subscriptionsBySessionId.getOrDefault(sessionId, Collections.emptyMap()).get(subscriptionId); TbSubscription subscription = subscriptionsBySessionId.getOrDefault(sessionId, Collections.emptyMap()).get(subscriptionId);
if (subscription != null && subscription.getType() == TbSubscriptionType.NOTIFICATIONS) { if (subscription != null && (subscription.getType() == TbSubscriptionType.NOTIFICATIONS
|| subscription.getType() == TbSubscriptionType.NOTIFICATIONS_COUNT)) {
subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update)); subscriptionUpdateExecutor.submit(() -> subscription.getUpdateProcessor().accept(subscription, update));
} }
callback.onSuccess(); callback.onSuccess();

2
application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java

@ -49,7 +49,7 @@ public interface SubscriptionManagerService extends ApplicationListener<Partitio
void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback);
void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, TbCallback callback); void onNotificationUpdate(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew, TbCallback callback);
void onNotificationRequestDeleted(TenantId tenantId, NotificationRequestId notificationRequestId, TbCallback callback); void onNotificationRequestDeleted(TenantId tenantId, NotificationRequestId notificationRequestId, TbCallback callback);

2
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.service.subscription; package org.thingsboard.server.service.subscription;
public enum TbSubscriptionType { public enum TbSubscriptionType {
TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS, NOTIFICATIONS_COUNT
} }

22
application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java

@ -53,6 +53,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscrip
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
@ -114,6 +116,11 @@ public class TbSubscriptionUtils {
.setSub(subscriptionProto) .setSub(subscriptionProto)
.setLimit(notificationsSub.getLimit())); .setLimit(notificationsSub.getLimit()));
break; break;
case NOTIFICATIONS_COUNT:
NotificationsCountSubscription notificationsCountSub = (NotificationsCountSubscription) subscription;
msgBuilder.setNotificationsCountSub(TransportProtos.NotificationsCountSubscriptionProto.newBuilder()
.setSub(subscriptionProto));
break;
} }
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build();
} }
@ -187,6 +194,17 @@ public class TbSubscriptionUtils {
.build(); .build();
} }
public static NotificationsCountSubscription fromProto(TransportProtos.NotificationsCountSubscriptionProto notificationsCountSub) {
TbSubscriptionProto sub = notificationsCountSub.getSub();
return NotificationsCountSubscription.builder()
.serviceId(sub.getServiceId())
.sessionId(sub.getSessionId())
.subscriptionId(sub.getSubscriptionId())
.tenantId(TenantId.fromUUID(new UUID(sub.getTenantIdMSB(), sub.getTenantIdLSB())))
.entityId(EntityIdFactory.getByTypeAndUuid(sub.getEntityType(), new UUID(sub.getEntityIdMSB(), sub.getEntityIdLSB())))
.build();
}
public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) { public static TelemetrySubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) {
if (proto.getErrorCode() > 0) { if (proto.getErrorCode() > 0) {
return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); return new TelemetrySubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
@ -367,6 +385,7 @@ public class TbSubscriptionUtils {
.setSessionId(subscription.getSessionId()) .setSessionId(subscription.getSessionId())
.setSubscriptionId(subscription.getSubscriptionId()) .setSubscriptionId(subscription.getSubscriptionId())
.setNotification(JacksonUtil.toString(update.getNotification())) .setNotification(JacksonUtil.toString(update.getNotification()))
.setIsNewNotification(update.isNewNotification())
.build(); .build();
return TransportProtos.ToCoreNotificationMsg.newBuilder() return TransportProtos.ToCoreNotificationMsg.newBuilder()
.setToLocalSubscriptionServiceMsg(TransportProtos.LocalSubscriptionServiceMsgProto.newBuilder() .setToLocalSubscriptionServiceMsg(TransportProtos.LocalSubscriptionServiceMsgProto.newBuilder()
@ -375,13 +394,14 @@ public class TbSubscriptionUtils {
.build(); .build();
} }
public static ToCoreMsg notificationUpdateToProto(TenantId tenantId, UserId recipientId, Notification notification) { public static ToCoreMsg notificationUpdateToProto(TenantId tenantId, UserId recipientId, Notification notification, boolean isNew) {
TransportProtos.NotificationUpdateProto updateProto = TransportProtos.NotificationUpdateProto.newBuilder() TransportProtos.NotificationUpdateProto updateProto = TransportProtos.NotificationUpdateProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRecipientIdMSB(recipientId.getId().getMostSignificantBits()) .setRecipientIdMSB(recipientId.getId().getMostSignificantBits())
.setRecipientIdLSB(recipientId.getId().getLeastSignificantBits()) .setRecipientIdLSB(recipientId.getId().getLeastSignificantBits())
.setNotification(JacksonUtil.toString(notification)) .setNotification(JacksonUtil.toString(notification))
.setIsNew(isNew)
.build(); .build();
return ToCoreMsg.newBuilder() return ToCoreMsg.newBuilder()
.setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder() .setToSubscriptionMgrMsg(SubscriptionMgrMsgProto.newBuilder()

193
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultWebSocketService.java

@ -22,8 +22,8 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
@ -65,13 +65,10 @@ import org.thingsboard.server.service.subscription.TbTimeseriesSubscription;
import org.thingsboard.server.service.ws.SessionEvent; import org.thingsboard.server.service.ws.SessionEvent;
import org.thingsboard.server.service.ws.WebSocketMsgEndpoint; import org.thingsboard.server.service.ws.WebSocketMsgEndpoint;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.WsSessionMetaData; import org.thingsboard.server.service.ws.WsSessionMetaData;
import org.thingsboard.server.service.ws.notification.DefaultNotificationCommandsHandler;
import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler; import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper; import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsUnsubCmd;
import org.thingsboard.server.service.ws.telemetry.WebSocketService; import org.thingsboard.server.service.ws.telemetry.WebSocketService;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
@ -105,6 +102,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -114,6 +112,7 @@ import java.util.stream.Collectors;
@Service @Service
@TbCoreComponent @TbCoreComponent
@Slf4j @Slf4j
@RequiredArgsConstructor
public class DefaultWebSocketService implements WebSocketService { public class DefaultWebSocketService implements WebSocketService {
public static final int NUMBER_OF_PING_ATTEMPTS = 3; public static final int NUMBER_OF_PING_ATTEMPTS = 3;
@ -130,53 +129,57 @@ public class DefaultWebSocketService implements WebSocketService {
private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
@Autowired private final TbLocalSubscriptionService oldSubService;
private TbLocalSubscriptionService oldSubService; private final TbEntityDataSubscriptionService entityDataSubService;
private final NotificationCommandsHandler notificationCmdsHandler;
@Autowired private final WebSocketMsgEndpoint msgEndpoint;
private TbEntityDataSubscriptionService entityDataSubService; private final AccessValidator accessValidator;
private final AttributesService attributesService;
@Autowired private final TimeseriesService tsService;
private NotificationCommandsHandler notificationCmdsHandler; private final TbServiceInfoProvider serviceInfoProvider;
private final TbTenantProfileCache tenantProfileCache;
@Autowired
private WebSocketMsgEndpoint msgEndpoint;
@Autowired
private AccessValidator accessValidator;
@Autowired
private AttributesService attributesService;
@Autowired
private TimeseriesService tsService;
@Autowired
private TbServiceInfoProvider serviceInfoProvider;
@Autowired
private TbTenantProfileCache tenantProfileCache;
@Value("${server.ws.ping_timeout:30000}") @Value("${server.ws.ping_timeout:30000}")
private long pingTimeout; private long pingTimeout;
private ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
private ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private ExecutorService executor; private ExecutorService executor;
private ScheduledExecutorService pingExecutor;
private String serviceId; private String serviceId;
private ScheduledExecutorService pingExecutor; private List<WsCmdListHandler<TelemetryPluginCmdsWrapper, ?>> telemetryCmdsHandlers;
private List<WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd>> notificationCmdsHandlers;
@PostConstruct @PostConstruct
public void initExecutor() { public void init() {
serviceId = serviceInfoProvider.getServiceId(); serviceId = serviceInfoProvider.getServiceId();
executor = ThingsBoardExecutors.newWorkStealingPool(50, getClass()); executor = ThingsBoardExecutors.newWorkStealingPool(50, getClass());
pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping")); pingExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("telemetry-web-socket-ping"));
pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS); pingExecutor.scheduleWithFixedDelay(this::sendPing, pingTimeout / NUMBER_OF_PING_ATTEMPTS, pingTimeout / NUMBER_OF_PING_ATTEMPTS, TimeUnit.MILLISECONDS);
telemetryCmdsHandlers = List.of(
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getAttrSubCmds, this::handleWsAttributesSubscriptionCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getTsSubCmds, this::handleWsTimeseriesSubscriptionCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getHistoryCmds, this::handleWsHistoryCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getEntityDataCmds, this::handleWsEntityDataCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getAlarmDataCmds, this::handleWsAlarmDataCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getEntityCountCmds, this::handleWsEntityCountCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getEntityDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getAlarmDataUnsubscribeCmds, this::handleWsDataUnsubscribeCmd),
WsCmdListHandler.of(TelemetryPluginCmdsWrapper::getEntityCountUnsubscribeCmds, this::handleWsDataUnsubscribeCmd)
);
notificationCmdsHandlers = List.of(
WsCmdHandler.of(NotificationCmdsWrapper::getUnreadSubCmd, notificationCmdsHandler::handleUnreadNotificationsSubCmd),
WsCmdHandler.of(NotificationCmdsWrapper::getUnreadCountSubCmd, notificationCmdsHandler::handleUnreadNotificationsCountSubCmd),
WsCmdHandler.of(NotificationCmdsWrapper::getMarkAsReadCmd, notificationCmdsHandler::handleMarkAsReadCmd),
WsCmdHandler.of(NotificationCmdsWrapper::getUnsubCmd, notificationCmdsHandler::handleUnsubCmd)
);
} }
@PreDestroy @PreDestroy
@ -231,77 +234,30 @@ public class DefaultWebSocketService implements WebSocketService {
} }
} }
private void processTelemetryCmds(WebSocketSessionRef sessionRef, String msg) throws JsonProcessingException { private void processTelemetryCmds(WebSocketSessionRef sessionRef, String msg) throws JsonProcessingException {
TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
if (cmdsWrapper == null) { if (cmdsWrapper == null) {
return; return;
} }
if (cmdsWrapper.getAttrSubCmds() != null) { for (WsCmdListHandler<TelemetryPluginCmdsWrapper, ?> cmdHandler : telemetryCmdsHandlers) {
cmdsWrapper.getAttrSubCmds().forEach(cmd -> { List<?> cmds = cmdHandler.extractCmds(cmdsWrapper);
if (processSubscription(sessionRef, cmd)) { if (cmds != null) {
handleWsAttributesSubscriptionCmd(sessionRef, cmd); cmdHandler.handle(sessionRef, cmds);
} }
});
}
if (cmdsWrapper.getTsSubCmds() != null) {
cmdsWrapper.getTsSubCmds().forEach(cmd -> {
if (processSubscription(sessionRef, cmd)) {
handleWsTimeseriesSubscriptionCmd(sessionRef, cmd);
}
});
}
if (cmdsWrapper.getHistoryCmds() != null) {
cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityDataCmds() != null) {
cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd));
}
if (cmdsWrapper.getAlarmDataCmds() != null) {
cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountCmds() != null) {
cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) {
cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
}
if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) {
cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
} }
} }
private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException { private void processNotificationCmds(WebSocketSessionRef sessionRef, String msg) throws IOException {
NotificationCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, NotificationCmdsWrapper.class); NotificationCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, NotificationCmdsWrapper.class);
if (cmdsWrapper.getUnreadSubCmd() != null) { for (WsCmdHandler<NotificationCmdsWrapper, ? extends WsCmd> cmdHandler : notificationCmdsHandlers) {
handleUnreadNotificationsSubCmd(sessionRef, cmdsWrapper.getUnreadSubCmd()); WsCmd cmd = cmdHandler.extractCmd(cmdsWrapper);
} else if (cmdsWrapper.getUnreadUnsubCmd() != null) { if (cmd != null) {
handleUnreadNotificationsUnsubCmd(sessionRef, cmdsWrapper.getUnreadUnsubCmd()); String sessionId = sessionRef.getSessionId();
} else if (cmdsWrapper.getMarkAsReadCmd() != null) { if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
handleMarkNotificationAsReadCmd(sessionRef, cmdsWrapper.getMarkAsReadCmd()); cmdHandler.handle(sessionRef, cmd); // todo: handle exceptions
} }
} }
private void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd) {
String sessionId = sessionRef.getSessionId();
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
notificationCmdsHandler.handleUnreadNotificationsSubCmd(sessionRef, cmd);
}
}
private void handleUnreadNotificationsUnsubCmd(WebSocketSessionRef sessionRef, NotificationsUnsubCmd cmd) {
String sessionId = sessionRef.getSessionId();
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
notificationCmdsHandler.handleUnsubCmd(sessionRef, cmd);
}
}
private void handleMarkNotificationAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationAsReadCmd cmd) {
String sessionId = sessionRef.getSessionId();
if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) {
notificationCmdsHandler.handleMarkAsReadCmd(sessionRef, cmd);
} }
} }
@ -479,6 +435,10 @@ public class DefaultWebSocketService implements WebSocketService {
} }
private void handleWsAttributesSubscriptionCmd(WebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) { private void handleWsAttributesSubscriptionCmd(WebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
if (!processSubscription(sessionRef, cmd)) {
return;
}
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd); log.debug("[{}] Processing: {}", sessionId, cmd);
@ -644,6 +604,10 @@ public class DefaultWebSocketService implements WebSocketService {
} }
private void handleWsTimeseriesSubscriptionCmd(WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) { private void handleWsTimeseriesSubscriptionCmd(WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
if (!processSubscription(sessionRef, cmd)) {
return;
}
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
log.debug("[{}] Processing: {}", sessionId, cmd); log.debug("[{}] Processing: {}", sessionId, cmd);
@ -990,4 +954,37 @@ public class DefaultWebSocketService implements WebSocketService {
private int getLimit(int limit) { private int getLimit(int limit) {
return limit == 0 ? DEFAULT_LIMIT : limit; return limit == 0 ? DEFAULT_LIMIT : limit;
} }
@RequiredArgsConstructor(staticName = "of")
public static class WsCmdHandler<W, C> {
private final java.util.function.Function<W, C> cmdExtractor;
private final BiConsumer<WebSocketSessionRef, C> handler;
public C extractCmd(W cmdsWrapper) {
return cmdExtractor.apply(cmdsWrapper);
}
@SuppressWarnings("unchecked")
public void handle(WebSocketSessionRef sessionRef, Object cmd) {
handler.accept(sessionRef, (C) cmd);
}
}
@RequiredArgsConstructor(staticName = "of")
public static class WsCmdListHandler<W, C> {
private final java.util.function.Function<W, List<C>> cmdExtractor;
private final BiConsumer<WebSocketSessionRef, C> handler;
public List<C> extractCmds(W cmdsWrapper) {
return cmdExtractor.apply(cmdsWrapper);
}
@SuppressWarnings("unchecked")
public void handle(WebSocketSessionRef sessionRef, List<?> cmds) {
cmds.forEach(cmd -> {
handler.accept(sessionRef, (C) cmd);
});
}
}
} }

20
application/src/main/java/org/thingsboard/server/service/ws/WsCmd.java

@ -0,0 +1,20 @@
/**
* 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.ws;
public interface WsCmd {
int getCmdId();
}

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

@ -16,6 +16,8 @@
package org.thingsboard.server.service.ws.notification; package org.thingsboard.server.service.ws.notification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.NotificationId;
@ -28,14 +30,16 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.notification.NotificationProcessingService; import org.thingsboard.server.dao.notification.NotificationProcessingService;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.subscription.NotificationsSubscription; import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscription;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsCountSubscription;
import org.thingsboard.server.service.ws.telemetry.WebSocketService; import org.thingsboard.server.service.ws.telemetry.WebSocketService;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
import java.util.Set; import java.util.Set;
@ -48,10 +52,11 @@ import java.util.stream.Collectors;
public class DefaultNotificationCommandsHandler implements NotificationCommandsHandler { public class DefaultNotificationCommandsHandler implements NotificationCommandsHandler {
private final NotificationService notificationService; private final NotificationService notificationService;
private final WebSocketService wsService;
private final TbLocalSubscriptionService localSubscriptionService; private final TbLocalSubscriptionService localSubscriptionService;
private final NotificationProcessingService notificationProcessingService; private final NotificationProcessingService notificationProcessingService;
private final TbServiceInfoProvider serviceInfoProvider; private final TbServiceInfoProvider serviceInfoProvider;
@Autowired @Lazy
private WebSocketService wsService;
@Override @Override
public void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd) { public void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd) {
@ -62,7 +67,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.subscriptionId(cmd.getCmdId()) .subscriptionId(cmd.getCmdId())
.tenantId(user.getTenantId()) .tenantId(user.getTenantId())
.entityId(user.getId()) .entityId(user.getId())
.updateProcessor(this::handleSubscriptionUpdate) .updateProcessor(this::handleNotificationsSubscriptionUpdate)
.limit(cmd.getLimit()) .limit(cmd.getLimit())
.build(); .build();
localSubscriptionService.addSubscription(subscription); localSubscriptionService.addSubscription(subscription);
@ -72,14 +77,20 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
} }
@Override @Override
public void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationAsReadCmd cmd) { public void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd) {
NotificationId notificationId = new NotificationId(cmd.getNotificationId()); SecurityUser user = sessionRef.getSecurityCtx();
notificationProcessingService.markNotificationAsRead(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), notificationId); NotificationsCountSubscription subscription = NotificationsCountSubscription.builder()
} .serviceId(serviceInfoProvider.getServiceId())
.sessionId(sessionRef.getSessionId())
.subscriptionId(cmd.getCmdId())
.tenantId(user.getTenantId())
.entityId(user.getId())
.updateProcessor(this::handleNotificationsCountSubscriptionUpdate)
.build();
localSubscriptionService.addSubscription(subscription);
@Override fetchUnreadNotificationsCount(subscription);
public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) { sendUpdate(sessionRef.getSessionId(), subscription.createUpdate());
localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId());
} }
private void fetchUnreadNotifications(NotificationsSubscription subscription) { private void fetchUnreadNotifications(NotificationsSubscription subscription) {
@ -87,19 +98,24 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
(UserId) subscription.getEntityId(), subscription.getLimit()); (UserId) subscription.getEntityId(), subscription.getLimit());
subscription.getUnreadNotifications().clear(); subscription.getUnreadNotifications().clear();
subscription.getUnreadNotifications().putAll(notifications.getData().stream().collect(Collectors.toMap(IdBased::getUuidId, n -> n))); subscription.getUnreadNotifications().putAll(notifications.getData().stream().collect(Collectors.toMap(IdBased::getUuidId, n -> n)));
subscription.getTotalUnreadCount().set((int) notifications.getTotalElements()); subscription.getTotalUnreadCounter().set((int) notifications.getTotalElements());
}
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
int unreadCount = notificationService.countUnreadNotificationsByUserId(subscription.getTenantId(), (UserId) subscription.getEntityId());
subscription.getUnreadCounter().set(unreadCount);
} }
private void handleSubscriptionUpdate(NotificationsSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) { private void handleNotificationsSubscriptionUpdate(NotificationsSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) {
if (subscriptionUpdate.getNotification() != null) { if (subscriptionUpdate.getNotification() != null) {
Notification notification = subscriptionUpdate.getNotification(); Notification notification = subscriptionUpdate.getNotification();
if (notification.getStatus() == NotificationStatus.READ) { if (notification.getStatus() == NotificationStatus.READ) {
fetchUnreadNotifications(subscription); fetchUnreadNotifications(subscription);
sendUpdate(subscription.getSessionId(), subscription.createFullUpdate()); sendUpdate(subscription.getSessionId(), subscription.createFullUpdate());
} else { } else {
Notification previous = subscription.getUnreadNotifications().put(notification.getUuidId(), notification); subscription.getUnreadNotifications().put(notification.getUuidId(), notification);
if (previous == null) { if (subscriptionUpdate.isNewNotification()) {
subscription.getTotalUnreadCount().incrementAndGet(); subscription.getTotalUnreadCounter().incrementAndGet();
Set<UUID> beyondLimit = subscription.getUnreadNotifications().keySet().stream() Set<UUID> beyondLimit = subscription.getUnreadNotifications().keySet().stream()
.skip(subscription.getLimit()) .skip(subscription.getLimit())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -116,8 +132,35 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
} }
} }
private void sendUpdate(String sessionId, UnreadNotificationsUpdate update) { private void handleNotificationsCountSubscriptionUpdate(NotificationsCountSubscription subscription, NotificationsSubscriptionUpdate subscriptionUpdate) {
if (subscriptionUpdate.getNotification() != null) {
Notification notification = subscriptionUpdate.getNotification();
if (subscriptionUpdate.isNewNotification()) {
subscription.getUnreadCounter().incrementAndGet();
} else if (notification.getStatus() == NotificationStatus.READ) {
// for now this can only happen when user marks notification as read
subscription.getUnreadCounter().decrementAndGet();
}
} else if (subscriptionUpdate.isNotificationRequestDeleted()) {
fetchUnreadNotificationsCount(subscription);
}
sendUpdate(subscription.getSessionId(), subscription.createUpdate());
}
private void sendUpdate(String sessionId, CmdUpdate update) {
wsService.sendWsMsg(sessionId, update); wsService.sendWsMsg(sessionId, update);
} }
@Override
public void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationAsReadCmd cmd) {
NotificationId notificationId = new NotificationId(cmd.getNotificationId());
notificationProcessingService.markNotificationAsRead(sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), notificationId);
}
@Override
public void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd) {
localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), cmd.getCmdId());
}
} }

3
application/src/main/java/org/thingsboard/server/service/ws/notification/NotificationCommandsHandler.java

@ -18,12 +18,15 @@ package org.thingsboard.server.service.ws.notification;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
public interface NotificationCommandsHandler { public interface NotificationCommandsHandler {
void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd); void handleUnreadNotificationsSubCmd(WebSocketSessionRef sessionRef, NotificationsSubCmd cmd);
void handleUnreadNotificationsCountSubCmd(WebSocketSessionRef sessionRef, NotificationsCountSubCmd cmd);
void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationAsReadCmd cmd); void handleMarkAsReadCmd(WebSocketSessionRef sessionRef, MarkNotificationAsReadCmd cmd);
void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd); void handleUnsubCmd(WebSocketSessionRef sessionRef, UnsubscribeCmd cmd);

3
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/MarkNotificationAsReadCmd.java

@ -18,13 +18,14 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import java.util.UUID; import java.util.UUID;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class MarkNotificationAsReadCmd { public class MarkNotificationAsReadCmd implements WsCmd {
private int cmdId; private int cmdId;
private UUID notificationId; private UUID notificationId;
} }

10
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationCmdsWrapper.java

@ -16,13 +16,15 @@
package org.thingsboard.server.service.ws.notification.cmd; package org.thingsboard.server.service.ws.notification.cmd;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
@Data @Data
public class NotificationCmdsWrapper { public class NotificationCmdsWrapper {
private NotificationsSubCmd unreadSubCmd;
private NotificationsUnsubCmd unreadUnsubCmd;
private NotificationsCountSubCmd unreadCountSubCmd;
private NotificationsSubCmd unreadSubCmd;
private MarkNotificationAsReadCmd markAsReadCmd; private MarkNotificationAsReadCmd markAsReadCmd;
private NotificationsUnsubCmd unsubCmd;
} }

29
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsCountSubCmd.java

@ -0,0 +1,29 @@
/**
* 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.ws.notification.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NotificationsCountSubCmd implements WsCmd {
private int cmdId;
}

4
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsSubCmd.java

@ -17,12 +17,14 @@ package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class NotificationsSubCmd { public class NotificationsSubCmd implements WsCmd {
private int cmdId; private int cmdId;
private int limit; private int limit;
} }

7
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/NotificationsUnsubCmd.java

@ -15,10 +15,15 @@
*/ */
package org.thingsboard.server.service.ws.notification.cmd; package org.thingsboard.server.service.ws.notification.cmd;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.service.ws.WsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
@Data @Data
public class NotificationsUnsubCmd implements UnsubscribeCmd { @NoArgsConstructor
@AllArgsConstructor
public class NotificationsUnsubCmd implements UnsubscribeCmd, WsCmd {
private int cmdId; private int cmdId;
} }

44
application/src/main/java/org/thingsboard/server/service/ws/notification/cmd/UnreadNotificationsCountUpdate.java

@ -0,0 +1,44 @@
/**
* 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.ws.notification.cmd;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
@Getter
public class UnreadNotificationsCountUpdate extends CmdUpdate {
private final int totalUnreadCount;
@Builder
@JsonCreator
public UnreadNotificationsCountUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("errorCode") int errorCode,
@JsonProperty("errorMsg") String errorMsg,
@JsonProperty("totalUnreadCount") int totalUnreadCount) {
super(cmdId, errorCode, errorMsg);
this.totalUnreadCount = totalUnreadCount;
}
@Override
public CmdUpdateType getCmdUpdateType() {
return CmdUpdateType.NOTIFICATIONS_COUNT;
}
}

47
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsCountSubscription.java

@ -0,0 +1,47 @@
/**
* 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.ws.notification.sub;
import lombok.Builder;
import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
@Getter
public class NotificationsCountSubscription extends TbSubscription<NotificationsSubscriptionUpdate> {
private final AtomicInteger unreadCounter = new AtomicInteger();
@Builder
public NotificationsCountSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
BiConsumer<NotificationsCountSubscription, NotificationsSubscriptionUpdate> updateProcessor) {
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.NOTIFICATIONS_COUNT, updateProcessor);
}
public UnreadNotificationsCountUpdate createUpdate() {
return UnreadNotificationsCountUpdate.builder()
.cmdId(getSubscriptionId())
.totalUnreadCount(unreadCounter.get())
.build();
}
}

11
application/src/main/java/org/thingsboard/server/service/subscription/NotificationsSubscription.java → application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscription.java

@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.service.subscription; package org.thingsboard.server.service.ws.notification.sub;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.service.subscription.TbSubscription;
import org.thingsboard.server.service.subscription.TbSubscriptionType;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -34,7 +35,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
private final Map<UUID, Notification> unreadNotifications = new LinkedHashMap<>(); private final Map<UUID, Notification> unreadNotifications = new LinkedHashMap<>();
private final int limit; private final int limit;
private final AtomicInteger totalUnreadCount = new AtomicInteger(); private final AtomicInteger totalUnreadCounter = new AtomicInteger();
@Builder @Builder
public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, public NotificationsSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
@ -48,7 +49,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
return UnreadNotificationsUpdate.builder() return UnreadNotificationsUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.notifications(unreadNotifications.values()) .notifications(unreadNotifications.values())
.totalUnreadCount(totalUnreadCount.get()) .totalUnreadCount(totalUnreadCounter.get())
.build(); .build();
} }
@ -56,7 +57,7 @@ public class NotificationsSubscription extends TbSubscription<NotificationsSubsc
return UnreadNotificationsUpdate.builder() return UnreadNotificationsUpdate.builder()
.cmdId(getSubscriptionId()) .cmdId(getSubscriptionId())
.update(notification) .update(notification)
.totalUnreadCount(totalUnreadCount.get()) .totalUnreadCount(totalUnreadCounter.get())
.build(); .build();
} }

1
application/src/main/java/org/thingsboard/server/service/ws/notification/sub/NotificationsSubscriptionUpdate.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.notification.Notification;
public class NotificationsSubscriptionUpdate { public class NotificationsSubscriptionUpdate {
private final Notification notification; private final Notification notification;
private final boolean isNewNotification;
private final boolean notificationRequestDeleted; private final boolean notificationRequestDeleted;
private final NotificationRequestId notificationRequestId; private final NotificationRequestId notificationRequestId;

3
application/src/main/java/org/thingsboard/server/service/ws/telemetry/cmd/v2/CmdUpdateType.java

@ -19,5 +19,6 @@ public enum CmdUpdateType {
ENTITY_DATA, ENTITY_DATA,
ALARM_DATA, ALARM_DATA,
COUNT_DATA, COUNT_DATA,
NOTIFICATIONS NOTIFICATIONS,
NOTIFICATIONS_COUNT
} }

1
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -89,6 +89,7 @@ public class TbTestWebSocketClient extends WebSocketClient {
@Override @Override
public void send(String text) throws NotYetConnectedException { public void send(String text) throws NotYetConnectedException {
log.info("SENDING: {}", text);
reply = new CountDownLatch(1); reply = new CountDownLatch(1);
super.send(text); super.send(text);
} }

50
application/src/test/java/org/thingsboard/server/service/notification/NotificationsWebSocketClient.java

@ -15,31 +15,31 @@
*/ */
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.RandomUtils;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd; import org.thingsboard.server.service.ws.notification.cmd.MarkNotificationAsReadCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper; import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsCountSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd; import org.thingsboard.server.service.ws.notification.cmd.NotificationsSubCmd;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdateType;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
public class NotificationsWebSocketClient extends TbTestWebSocketClient { public class NotificationsWebSocketClient extends TbTestWebSocketClient {
private final Map<UUID, Notification> currentNotifications = new LinkedHashMap<>();
@Getter @Getter
private int totalUnreadCount; private UnreadNotificationsUpdate lastDataUpdate;
@Getter @Getter
private UnreadNotificationsUpdate lastUpdate; private UnreadNotificationsCountUpdate lastCountUpdate;
public NotificationsWebSocketClient(String wsUrl, String token) throws URISyntaxException { public NotificationsWebSocketClient(String wsUrl, String token) throws URISyntaxException {
super(new URI(wsUrl + "/api/ws/plugins/notifications?token=" + token)); super(new URI(wsUrl + "/api/ws/plugins/notifications?token=" + token));
@ -47,38 +47,36 @@ public class NotificationsWebSocketClient extends TbTestWebSocketClient {
public void subscribeForUnreadNotifications(int limit) { public void subscribeForUnreadNotifications(int limit) {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setUnreadSubCmd(new NotificationsSubCmd(newCmdId(), limit)); cmdsWrapper.setUnreadSubCmd(new NotificationsSubCmd(1, limit));
sendCmd(cmdsWrapper); sendCmd(cmdsWrapper);
} }
public void markNotificationAsRead(UUID notificationId) { public void subscribeForUnreadNotificationsCount() {
NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper(); NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
cmdsWrapper.setMarkAsReadCmd(new MarkNotificationAsReadCmd(newCmdId(), notificationId)); cmdsWrapper.setUnreadCountSubCmd(new NotificationsCountSubCmd(2));
sendCmd(cmdsWrapper); sendCmd(cmdsWrapper);
} }
public void markNotificationAsRead(UUID notificationId) {
private void handleUpdate(UnreadNotificationsUpdate update) { NotificationCmdsWrapper cmdsWrapper = new NotificationCmdsWrapper();
totalUnreadCount = update.getTotalUnreadCount(); cmdsWrapper.setMarkAsReadCmd(new MarkNotificationAsReadCmd(newCmdId(), notificationId));
if (update.getNotifications() != null) { sendCmd(cmdsWrapper);
currentNotifications.clear();
currentNotifications.putAll(update.getNotifications().stream().collect(Collectors.toMap(IdBased::getUuidId, n -> n)));
} else if (update.getUpdate() != null) {
Notification notification = update.getUpdate();
currentNotifications.put(notification.getUuidId(), notification);
}
} }
public void sendCmd(NotificationCmdsWrapper cmdsWrapper) { public void sendCmd(NotificationCmdsWrapper cmdsWrapper) {
send(JacksonUtil.toString(cmdsWrapper)); String cmd = JacksonUtil.toString(cmdsWrapper);
send(cmd);
} }
@Override @Override
public void onMessage(String s) { public void onMessage(String s) {
UnreadNotificationsUpdate update = JacksonUtil.fromString(s, UnreadNotificationsUpdate.class); JsonNode update = JacksonUtil.toJsonNode(s);
lastUpdate = update; CmdUpdateType updateType = CmdUpdateType.valueOf(update.get("cmdUpdateType").asText());
handleUpdate(update); if (updateType == CmdUpdateType.NOTIFICATIONS) {
lastDataUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsUpdate.class);
} else if (updateType == CmdUpdateType.NOTIFICATIONS_COUNT) {
lastCountUpdate = JacksonUtil.treeToValue(update, UnreadNotificationsCountUpdate.class);
}
super.onMessage(s); super.onMessage(s);
} }

67
application/src/test/java/org/thingsboard/server/service/notification/NotificationsWsApiTest.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.notification.targets.SingleUserNotific
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCountUpdate;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -41,6 +42,41 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
} }
@Test
public void testSubscribingToUnreadNotificationsCount() {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText1);
String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText2);
getWsClient().subscribeForUnreadNotificationsCount();
getWsClient().waitForReply();
UnreadNotificationsCountUpdate update = getWsClient().getLastCountUpdate();
assertThat(update.getTotalUnreadCount()).isEqualTo(2);
}
@Test
public void testReceivingCountUpdates_multipleSessions() {
getWsClient().subscribeForUnreadNotificationsCount();
getAnotherWsClient().subscribeForUnreadNotificationsCount();
getWsClient().waitForReply();
getAnotherWsClient().waitForReply();
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
String notificationText = "Notification";
submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText);
getWsClient().waitForUpdate();
getAnotherWsClient().waitForUpdate();
assertThat(getWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
}
@Test @Test
public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception { public void testSubscribingToUnreadNotifications_multipleSessions() throws Exception {
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
@ -54,8 +90,8 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
getWsClient().waitForReply(); getWsClient().waitForReply();
getAnotherWsClient().waitForReply(); getAnotherWsClient().waitForReply();
checkFullNotificationsUpdate(getWsClient().getLastUpdate(), notificationText1, notificationText2); checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText1, notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastUpdate(), notificationText1, notificationText2); checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText1, notificationText2);
} }
@Test @Test
@ -64,7 +100,7 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
getAnotherWsClient().subscribeForUnreadNotifications(10); getAnotherWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(); getWsClient().waitForReply();
getAnotherWsClient().waitForReply(); getAnotherWsClient().waitForReply();
UnreadNotificationsUpdate notificationsUpdate = getWsClient().getLastUpdate(); UnreadNotificationsUpdate notificationsUpdate = getWsClient().getLastDataUpdate();
assertThat(notificationsUpdate.getTotalUnreadCount()).isZero(); assertThat(notificationsUpdate.getTotalUnreadCount()).isZero();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
@ -75,8 +111,8 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
getWsClient().waitForUpdate(); getWsClient().waitForUpdate();
getAnotherWsClient().waitForUpdate(); getAnotherWsClient().waitForUpdate();
checkPartialNotificationsUpdate(getWsClient().getLastUpdate(), notificationText, 1); checkPartialNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText, 1);
checkPartialNotificationsUpdate(getAnotherWsClient().getLastUpdate(), notificationText, 1); checkPartialNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText, 1);
} }
@Test @Test
@ -85,28 +121,37 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
getAnotherWsClient().subscribeForUnreadNotifications(10); getAnotherWsClient().subscribeForUnreadNotifications(10);
getWsClient().waitForReply(); getWsClient().waitForReply();
getAnotherWsClient().waitForReply(); getAnotherWsClient().waitForReply();
getAnotherWsClient().subscribeForUnreadNotificationsCount();
getAnotherWsClient().waitForReply();
NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId); NotificationTarget notificationTarget = createNotificationTarget(tenantAdminUserId);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2);
String notificationText1 = "Notification 1"; String notificationText1 = "Notification 1";
submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText1); submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText1);
getWsClient().waitForUpdate(); getWsClient().waitForUpdate();
Notification notification1 = getWsClient().getLastUpdate().getUpdate(); getAnotherWsClient().waitForUpdate();
Notification notification1 = getWsClient().getLastDataUpdate().getUpdate();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(2);
String notificationText2 = "Notification 2"; String notificationText2 = "Notification 2";
submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText2); submitNotificationRequest(notificationTarget.getId(), "Just a test", notificationText2);
getWsClient().waitForUpdate(); getWsClient().waitForUpdate();
assertThat(getWsClient().getLastUpdate().getTotalUnreadCount()).isEqualTo(2); getAnotherWsClient().waitForUpdate();
assertThat(getWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastDataUpdate().getTotalUnreadCount()).isEqualTo(2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isEqualTo(2);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
getAnotherWsClient().registerWaitForUpdate(); getAnotherWsClient().registerWaitForUpdate(2);
getWsClient().markNotificationAsRead(notification1.getUuidId()); getWsClient().markNotificationAsRead(notification1.getUuidId());
getWsClient().waitForUpdate(); getWsClient().waitForUpdate();
getAnotherWsClient().waitForUpdate(); getAnotherWsClient().waitForUpdate();
checkFullNotificationsUpdate(getWsClient().getLastUpdate(), notificationText2); checkFullNotificationsUpdate(getWsClient().getLastDataUpdate(), notificationText2);
checkFullNotificationsUpdate(getAnotherWsClient().getLastUpdate(), notificationText2); checkFullNotificationsUpdate(getAnotherWsClient().getLastDataUpdate(), notificationText2);
assertThat(getAnotherWsClient().getLastCountUpdate().getTotalUnreadCount()).isOne();
} }
public void testReceivingUpdatesWhenSubscriptionAtAnotherInstance() {} public void testReceivingUpdatesWhenSubscriptionAtAnotherInstance() {}
@ -127,7 +172,7 @@ public class NotificationsWsApiTest extends AbstractControllerTest {
notificationTarget.setTenantId(tenantId); notificationTarget.setTenantId(tenantId);
notificationTarget.setName("User " + userId); notificationTarget.setName("User " + userId);
SingleUserNotificationTargetConfig config = new SingleUserNotificationTargetConfig(); SingleUserNotificationTargetConfig config = new SingleUserNotificationTargetConfig();
config.setUserId(userId); config.setUserId(userId.getId());
notificationTarget.setConfiguration(config); notificationTarget.setConfiguration(config);
return doPost("/api/notification/target", notificationTarget, NotificationTarget.class); return doPost("/api/notification/target", notificationTarget, NotificationTarget.class);
} }

11
common/cluster-api/src/main/proto/queue.proto

@ -552,6 +552,10 @@ message NotificationsSubscriptionProto {
int32 limit = 2; int32 limit = 2;
} }
message NotificationsCountSubscriptionProto {
TbSubscriptionProto sub = 1;
}
message TbSubscriptionUpdateProto { message TbSubscriptionUpdateProto {
string sessionId = 1; string sessionId = 1;
int32 subscriptionId = 2; int32 subscriptionId = 2;
@ -573,6 +577,7 @@ message NotificationsSubscriptionUpdateProto {
string sessionId = 1; string sessionId = 1;
int32 subscriptionId = 2; int32 subscriptionId = 2;
string notification = 3; string notification = 3;
bool isNewNotification = 4;
} }
message NotificationUpdateProto { message NotificationUpdateProto {
@ -581,6 +586,7 @@ message NotificationUpdateProto {
int64 recipientIdMSB = 3; int64 recipientIdMSB = 3;
int64 recipientIdLSB = 4; int64 recipientIdLSB = 4;
string notification = 5; string notification = 5;
bool isNew = 6;
} }
message NotificationRequestDeleteProto { message NotificationRequestDeleteProto {
@ -692,8 +698,9 @@ message SubscriptionMgrMsgProto {
TbAlarmDeleteProto alarmDelete = 9; TbAlarmDeleteProto alarmDelete = 9;
TbTimeSeriesDeleteProto tsDelete = 10; TbTimeSeriesDeleteProto tsDelete = 10;
NotificationsSubscriptionProto notificationsSub = 11; NotificationsSubscriptionProto notificationsSub = 11;
NotificationUpdateProto notificationUpdate = 12; NotificationsCountSubscriptionProto notificationsCountSub = 12;
NotificationRequestDeleteProto notificationRequestDelete = 13; NotificationUpdateProto notificationUpdate = 13;
NotificationRequestDeleteProto notificationRequestDelete = 14;
} }
message LocalSubscriptionServiceMsgProto { message LocalSubscriptionServiceMsgProto {

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

@ -38,10 +38,14 @@ public interface NotificationService {
Notification createNotification(TenantId tenantId, Notification notification); Notification createNotification(TenantId tenantId, Notification notification);
Notification updateNotificationStatus(TenantId tenantId, NotificationId notificationId, NotificationStatus status); Notification findNotificationById(TenantId tenantId, NotificationId notificationId);
boolean updateNotificationStatus(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status);
PageData<Notification> findNotificationsByUserIdAndReadStatusAndPageLink(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink); PageData<Notification> findNotificationsByUserIdAndReadStatusAndPageLink(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink);
PageData<Notification> findLatestUnreadNotificationsByUserId(TenantId tenantId, UserId userId, int limit); PageData<Notification> findLatestUnreadNotificationsByUserId(TenantId tenantId, UserId userId, int limit);
int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId);
} }

9
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationTargetService.java

@ -15,15 +15,14 @@
*/ */
package org.thingsboard.server.dao.notification; package org.thingsboard.server.dao.notification;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import java.util.List;
public interface NotificationTargetService { public interface NotificationTargetService {
NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget); NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget);
@ -32,7 +31,9 @@ public interface NotificationTargetService {
PageData<NotificationTarget> findNotificationTargetsByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); PageData<NotificationTarget> findNotificationTargetsByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink);
List<UserId> findRecipientsForNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId); PageData<User> findRecipientsForNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId, PageLink pageLink);
PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, NotificationTargetConfig targetConfig, PageLink pageLink);
void deleteNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId); void deleteNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -57,6 +57,8 @@ public interface UserService {
PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink); PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink);
PageData<User> findUsers(TenantId tenantId, PageLink pageLink);
void deleteTenantAdmins(TenantId tenantId); void deleteTenantAdmins(TenantId tenantId);
PageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink); PageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink);

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

@ -21,11 +21,11 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import javax.validation.Valid; import javax.validation.Valid;
@ -37,7 +37,7 @@ import javax.validation.constraints.NotNull;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class NotificationRequest extends BaseData<NotificationRequestId> implements HasTenantId { public class NotificationRequest extends BaseData<NotificationRequestId> implements HasTenantId, HasName {
private TenantId tenantId; private TenantId tenantId;
@NotNull(message = "Target is not specified") @NotNull(message = "Target is not specified")
@ -55,4 +55,9 @@ public class NotificationRequest extends BaseData<NotificationRequestId> impleme
public static final String GENERAL_NOTIFICATION_REASON = "General"; public static final String GENERAL_NOTIFICATION_REASON = "General";
public static final String ALARM_NOTIFICATION_REASON = "Alarm"; public static final String ALARM_NOTIFICATION_REASON = "Alarm";
@Override
public String getName() {
return notificationReason;
}
} }

28
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/AllUsersNotificationTargetConfig.java

@ -0,0 +1,28 @@
/**
* 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.common.data.notification.targets;
import lombok.Data;
@Data
public class AllUsersNotificationTargetConfig implements NotificationTargetConfig {
@Override
public NotificationTargetConfigType getType() {
return NotificationTargetConfigType.ALL_USERS;
}
}

32
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/CustomerUsersNotificationTargetConfig.java

@ -0,0 +1,32 @@
/**
* 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.common.data.notification.targets;
import lombok.Data;
import java.util.UUID;
@Data
public class CustomerUsersNotificationTargetConfig implements NotificationTargetConfig {
private UUID customerId;
@Override
public NotificationTargetConfigType getType() {
return NotificationTargetConfigType.CUSTOMER_USERS;
}
}

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

@ -24,7 +24,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({ @JsonSubTypes({
@Type(value = SingleUserNotificationTargetConfig.class, name = "SINGLE_USER"), @Type(value = SingleUserNotificationTargetConfig.class, name = "SINGLE_USER"),
@Type(value = UserListNotificationTargetConfig.class, name = "USER_LIST") @Type(value = UserListNotificationTargetConfig.class, name = "USER_LIST"),
@Type(value = CustomerUsersNotificationTargetConfig.class, name = "CUSTOMER_USERS"),
@Type(value = AllUsersNotificationTargetConfig.class, name = "ALL_USERS")
}) })
public interface NotificationTargetConfig { public interface NotificationTargetConfig {

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

@ -19,6 +19,9 @@ public enum NotificationTargetConfigType {
SINGLE_USER, SINGLE_USER,
USER_LIST, USER_LIST,
CUSTOMER_USERS,
ALL_USERS
// USER_GROUP, // USER_GROUP,
// USERS_WITH_ROLE, // USERS_WITH_ROLE,
// QUERY // ? // QUERY // ?

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/SingleUserNotificationTargetConfig.java

@ -16,15 +16,15 @@
package org.thingsboard.server.common.data.notification.targets; package org.thingsboard.server.common.data.notification.targets;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.UserId;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.UUID;
@Data @Data
public class SingleUserNotificationTargetConfig implements NotificationTargetConfig { public class SingleUserNotificationTargetConfig implements NotificationTargetConfig {
@NotNull @NotNull
private UserId userId; private UUID userId;
@Override @Override
public NotificationTargetConfigType getType() { public NotificationTargetConfigType getType() {

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/UserListNotificationTargetConfig.java

@ -18,12 +18,15 @@ package org.thingsboard.server.common.data.notification.targets;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import javax.validation.constraints.NotEmpty;
import java.util.List; import java.util.List;
import java.util.UUID;
@Data @Data
public class UserListNotificationTargetConfig implements NotificationTargetConfig { public class UserListNotificationTargetConfig implements NotificationTargetConfig {
private List<UserId> usersIds; @NotEmpty
private List<UUID> usersIds;
@Override @Override
public NotificationTargetConfigType getType() { public NotificationTargetConfigType getType() {

6
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -110,13 +110,17 @@ public abstract class DaoUtil {
} }
public static <T> void processInBatches(Function<PageLink, PageData<T>> finder, int batchSize, Consumer<T> processor) { public static <T> void processInBatches(Function<PageLink, PageData<T>> finder, int batchSize, Consumer<T> processor) {
processBatches(finder, batchSize, batch -> batch.forEach(processor));
}
public static <T> void processBatches(Function<PageLink, PageData<T>> finder, int batchSize, Consumer<List<T>> processor) {
PageLink pageLink = new PageLink(batchSize); PageLink pageLink = new PageLink(batchSize);
PageData<T> batch; PageData<T> batch;
boolean hasNextBatch; boolean hasNextBatch;
do { do {
batch = finder.apply(pageLink); batch = finder.apply(pageLink);
batch.getData().forEach(processor); processor.accept(batch.getData());
hasNextBatch = batch.hasNext(); hasNextBatch = batch.hasNext();
pageLink = pageLink.nextPageLink(); pageLink = pageLink.nextPageLink();

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

@ -81,13 +81,17 @@ public class DefaultNotificationService implements NotificationService {
return notificationDao.save(tenantId, notification); return notificationDao.save(tenantId, notification);
} }
@Transactional
@Override @Override
public Notification updateNotificationStatus(TenantId tenantId, NotificationId notificationId, NotificationStatus status) { public Notification findNotificationById(TenantId tenantId, NotificationId notificationId) {
notificationDao.updateStatus(tenantId, notificationId, status);
return notificationDao.findById(tenantId, notificationId.getId()); return notificationDao.findById(tenantId, notificationId.getId());
} }
@Transactional
@Override
public boolean updateNotificationStatus(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status) {
return notificationDao.updateStatusByIdAndUserId(tenantId, userId, notificationId, status);
}
@Override @Override
public PageData<Notification> findNotificationsByUserIdAndReadStatusAndPageLink(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink) { public PageData<Notification> findNotificationsByUserIdAndReadStatusAndPageLink(TenantId tenantId, UserId userId, boolean unreadOnly, PageLink pageLink) {
if (unreadOnly) { if (unreadOnly) {
@ -104,6 +108,11 @@ public class DefaultNotificationService implements NotificationService {
return findNotificationsByUserIdAndReadStatusAndPageLink(tenantId, userId, true, pageLink); return findNotificationsByUserIdAndReadStatusAndPageLink(tenantId, userId, true, pageLink);
} }
@Override
public int countUnreadNotificationsByUserId(TenantId tenantId, UserId userId) {
return notificationDao.countUnreadByUserId(tenantId, userId);
}
private static class NotificationRequestValidator extends DataValidator<NotificationRequest> { private static class NotificationRequestValidator extends DataValidator<NotificationRequest> {
@Override @Override

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

@ -18,9 +18,12 @@ package org.thingsboard.server.dao.notification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.targets.CustomerUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.SingleUserNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.SingleUserNotificationTargetConfig;
@ -28,9 +31,10 @@ import org.thingsboard.server.common.data.notification.targets.UserListNotificat
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@ -38,11 +42,11 @@ import java.util.List;
public class DefaultNotificationTargetService implements NotificationTargetService { public class DefaultNotificationTargetService implements NotificationTargetService {
private final NotificationTargetDao notificationTargetDao; private final NotificationTargetDao notificationTargetDao;
private final UserService userService;
private final NotificationTargetValidator validator = new NotificationTargetValidator(); private final NotificationTargetValidator validator = new NotificationTargetValidator();
@Override @Override
public NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget) { public NotificationTarget saveNotificationTarget(TenantId tenantId, NotificationTarget notificationTarget) {
notificationTarget.setTenantId(tenantId);
validator.validate(notificationTarget, NotificationTarget::getTenantId); validator.validate(notificationTarget, NotificationTarget::getTenantId);
return notificationTargetDao.save(tenantId, notificationTarget); return notificationTargetDao.save(tenantId, notificationTarget);
} }
@ -58,21 +62,42 @@ public class DefaultNotificationTargetService implements NotificationTargetServi
} }
@Override @Override
public List<UserId> findRecipientsForNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId) { public PageData<User> findRecipientsForNotificationTarget(TenantId tenantId, NotificationTargetId notificationTargetId, PageLink pageLink) {
NotificationTarget notificationTarget = findNotificationTargetById(tenantId, notificationTargetId); NotificationTarget notificationTarget = findNotificationTargetById(tenantId, notificationTargetId);
NotificationTargetConfig configuration = notificationTarget.getConfiguration(); NotificationTargetConfig configuration = notificationTarget.getConfiguration();
List<UserId> recipients = new ArrayList<>(); return findRecipientsForNotificationTargetConfig(tenantId, configuration, pageLink);
switch (configuration.getType()) { }
case SINGLE_USER:
SingleUserNotificationTargetConfig singleUserNotificationTargetConfig = (SingleUserNotificationTargetConfig) configuration; @Override
recipients.add(singleUserNotificationTargetConfig.getUserId()); public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, NotificationTargetConfig targetConfig, PageLink pageLink) {
break; switch (targetConfig.getType()) {
case USER_LIST: case SINGLE_USER: {
UserListNotificationTargetConfig userListNotificationTargetConfig = (UserListNotificationTargetConfig) configuration; UserId userId = new UserId(((SingleUserNotificationTargetConfig) targetConfig).getUserId());
recipients.addAll(userListNotificationTargetConfig.getUsersIds()); User user = userService.findUserById(tenantId, userId);
break; 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))
.collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false);
}
case CUSTOMER_USERS: {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
throw new IllegalArgumentException("Customer users target is not supported for system administrator");
}
CustomerId customerId = new CustomerId(((CustomerUsersNotificationTargetConfig) targetConfig).getCustomerId());
return userService.findCustomerUsers(tenantId, customerId, pageLink);
}
case ALL_USERS: {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return userService.findUsersByTenantId(tenantId, pageLink);
} else {
return userService.findUsers(TenantId.SYS_TENANT_ID, pageLink);
}
}
} }
return recipients; return new PageData<>();
} }
@Override @Override

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

@ -30,6 +30,8 @@ public interface NotificationDao extends Dao<Notification> {
PageData<Notification> findByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink); PageData<Notification> findByUserIdAndPageLink(TenantId tenantId, UserId userId, PageLink pageLink);
void updateStatus(TenantId tenantId, NotificationId notificationId, NotificationStatus status); boolean updateStatusByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status);
int countUnreadByUserId(TenantId tenantId, UserId userId);
} }

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

@ -73,8 +73,13 @@ public class JpaNotificationDao extends JpaAbstractDao<NotificationEntity, Notif
} }
@Override @Override
public void updateStatus(TenantId tenantId, NotificationId notificationId, NotificationStatus status) { public boolean updateStatusByIdAndUserId(TenantId tenantId, UserId userId, NotificationId notificationId, NotificationStatus status) {
notificationRepository.updateStatus(notificationId.getId(), status); return notificationRepository.updateStatusByIdAndRecipientId(notificationId.getId(), userId.getId(), status) != 0;
}
@Override
public int countUnreadByUserId(TenantId tenantId, UserId userId) {
return notificationRepository.countByRecipientIdAndStatusNot(userId.getId(), NotificationStatus.READ);
} }
@Override @Override

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

@ -37,8 +37,13 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Modifying @Modifying
@Transactional @Transactional
@Query("UPDATE NotificationEntity n SET n.status = :status WHERE n.id = :id") @Query("UPDATE NotificationEntity n SET n.status = :status " +
void updateStatus(@Param("id") UUID id, @Param("status") NotificationStatus status); "WHERE n.id = :id AND n.recipientId = :recipientId AND n.status <> :status")
int updateStatusByIdAndRecipientId(@Param("id") UUID id,
@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status);
int countByRecipientIdAndStatusNot(UUID recipientId, NotificationStatus status);
void deleteByRequestId(UUID requestId); void deleteByRequestId(UUID requestId);

5
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java

@ -95,6 +95,11 @@ public class JpaUserDao extends JpaAbstractSearchTextDao<UserEntity, User> imple
} }
@Override
public PageData<User> findAll(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(userRepository.findAll(DaoUtil.toPageable(pageLink)));
}
@Override @Override
public Long countByTenantId(TenantId tenantId) { public Long countByTenantId(TenantId tenantId) {
return userRepository.countByTenantId(tenantId.getId()); return userRepository.countByTenantId(tenantId.getId());

3
dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java

@ -69,4 +69,7 @@ public interface UserDao extends Dao<User>, TenantEntityDao {
* @return the list of user entities * @return the list of user entities
*/ */
PageData<User> findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink); PageData<User> findCustomerUsers(UUID tenantId, UUID customerId, PageLink pageLink);
PageData<User> findAll(TenantId tenantId, PageLink pageLink);
} }

5
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -230,6 +230,11 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
return userDao.findTenantAdmins(tenantId.getId(), pageLink); return userDao.findTenantAdmins(tenantId.getId(), pageLink);
} }
@Override
public PageData<User> findUsers(TenantId tenantId, PageLink pageLink) {
return userDao.findAll(tenantId, pageLink);
}
@Override @Override
public void deleteTenantAdmins(TenantId tenantId) { public void deleteTenantAdmins(TenantId tenantId) {
log.trace("Executing deleteTenantAdmins, tenantId [{}]", tenantId); log.trace("Executing deleteTenantAdmins, tenantId [{}]", tenantId);

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -78,6 +78,8 @@ CREATE INDEX IF NOT EXISTS idx_notification_target_tenant_id_and_created_time ON
CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_and_created_time ON notification_request(tenant_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_request_tenant_id_and_created_time ON notification_request(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_created_time ON notification(recipient_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_status_and_created_time ON notification(recipient_id, status, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_and_status_and_created_time ON notification(recipient_id, status, created_time DESC);

Loading…
Cancel
Save