diff --git a/application/src/main/data/upgrade/3.4.3/schema_update.sql b/application/src/main/data/upgrade/3.4.3/schema_update.sql new file mode 100644 index 0000000000..bdc9d1b4fc --- /dev/null +++ b/application/src/main/data/upgrade/3.4.3/schema_update.sql @@ -0,0 +1,26 @@ +-- +-- 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. +-- + +CREATE TABLE IF NOT EXISTS alarm_comment ( + id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_id uuid NOT NULL, + user_id uuid, + type varchar(255) NOT NULL, + comment varchar(1000000), + CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +) PARTITION BY RANGE (created_time); +CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id); \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 8b19a6954c..8fb9804e71 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -278,6 +279,10 @@ public class ActorSystemContext { @Getter private AlarmSubscriptionService alarmService; + @Autowired + @Getter + private AlarmCommentService alarmCommentService; + @Autowired @Getter private JsInvokeService jsInvokeService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ba1c4e9796..b2338bd070 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -71,6 +71,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -591,6 +592,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getAlarmService(); } + @Override + public AlarmCommentService getAlarmCommentService() { + return mainCtx.getAlarmCommentService(); + } + @Override public RuleChainService getRuleChainService() { return mainCtx.getRuleChainService(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java new file mode 100644 index 0000000000..4a586027b4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java @@ -0,0 +1,129 @@ +/** + * 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.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api") +public class AlarmCommentController extends BaseController { + public static final String ALARM_ID = "alarmId"; + public static final String ALARM_COMMENT_ID = "commentId"; + + private final TbAlarmCommentService tbAlarmCommentService; + + @ApiOperation(value = "Create or update Alarm Comment ", + notes = "Creates or Updates the Alarm Comment. " + + "When creating comment, platform generates Alarm Comment Id as " + UUID_WIKI_LINK + + "The newly created Alarm Comment id will be present in the response. Specify existing Alarm Comment id to update the alarm. " + + "Referencing non-existing Alarm Comment Id will cause 'Not Found' error. " + + "\n\n To create new Alarm comment entity it is enough to specify 'comment' json element with 'text' node, for example: {\"comment\": { \"text\": \"my comment\"}}. " + + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) + @ResponseBody + public AlarmComment saveAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + alarmComment.setAlarmId(alarmId); + return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", + notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) + @ResponseBody + public Boolean deleteAlarmComment(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @ApiParam(value = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.DELETE); + + AlarmCommentId alarmCommentId = new AlarmCommentId(toUUID(strCommentId)); + AlarmComment alarmComment = checkAlarmCommentId(alarmCommentId); + return tbAlarmCommentService.deleteAlarmComment(alarm, alarmComment, getCurrentUser()); + } + + @ApiOperation(value = "Get Alarm comments (getAlarmComments)", + notes = "Returns a page of alarm comments for specified alarm. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) + @ResponseBody + public PageData getAlarmComments( + @ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); + checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found"); + checkEntityId(alarm.getOriginator(), Operation.READ); + + PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); + return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 43d049926b..eecf507a90 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; @@ -64,6 +65,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -98,6 +100,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -200,6 +203,9 @@ public abstract class BaseController { @Autowired protected AlarmSubscriptionService alarmService; + @Autowired + protected AlarmCommentService alarmCommentService; + @Autowired protected DeviceCredentialsService deviceCredentialsService; @@ -716,6 +722,17 @@ public abstract class BaseController { } } + AlarmComment checkAlarmCommentId(AlarmCommentId alarmCommentId) throws ThingsboardException { + try { + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + AlarmComment alarmComment = alarmCommentService.findAlarmCommentByIdAsync(getCurrentUser().getTenantId(), alarmCommentId).get(); + checkNotNull(alarmComment, "Alarm comment with id [" + alarmCommentId + "] is not found"); + return alarmComment; + } catch (Exception e) { + throw handleException(e, false); + } + } + WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, Operation operation) throws ThingsboardException { try { validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 2409a19ac5..0aafe5e368 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -44,6 +44,8 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + + protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'"; @@ -100,6 +102,7 @@ public class ControllerConstants { protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault"; protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status"; + protected static final String ALARM_COMMENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime"; protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id"; protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root"; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 9f34c061fe..10af591647 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -65,6 +66,8 @@ public abstract class AbstractTbEntityService { @Autowired protected AlarmSubscriptionService alarmSubscriptionService; @Autowired + protected AlarmCommentService alarmCommentService; + @Autowired protected CustomerService customerService; @Autowired protected TbClusterService tbClusterService; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 8f8531a5b4..c06d6e8d04 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -229,6 +230,16 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType)); } + @Override + public void notifyCreateOrUpdateAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user) { + logEntityAction(alarm.getTenantId(), alarmComment.getId(), alarmComment, alarm.getCustomerId(), actionType, user); + } + + @Override + public void notifyDeleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) { + logEntityAction(alarm.getTenantId(), alarmComment.getId(), alarmComment, alarm.getCustomerId(), ActionType.DELETED, user); + } + @Override public void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java index dc6e148cab..6ef53fd7f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -102,6 +103,10 @@ public interface TbNotificationEntityService { void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, User user, Object... additionalInfo); + void notifyCreateOrUpdateAlarmComment(Alarm alarm, AlarmComment alarmComment, ActionType actionType, User user); + + void notifyDeleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user); + void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId, I entityId, E entity, User user, ActionType actionType, boolean sendNotifyMsgToEdge, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java new file mode 100644 index 0000000000..5904745088 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -0,0 +1,52 @@ +/** + * 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.entitiy.alarm; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@AllArgsConstructor +public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ + @Override + public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException { + ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + UserId userId = user.getId(); + alarmComment.setUserId(userId); + try { + AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment).getAlarmComment()); + notificationEntityService.notifyCreateOrUpdateAlarmComment(alarm, savedAlarmComment, actionType, user); + return savedAlarmComment; + } catch (Exception e) { + notificationEntityService.logEntityAction(alarm.getTenantId(), emptyId(EntityType.ALARM_COMMENT), alarmComment, actionType, user, e); + throw e; + } + } + + @Override + public Boolean deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) { + notificationEntityService.notifyDeleteAlarmComment(alarm, alarmComment, user); + return alarmCommentService.deleteAlarmComment(alarm.getTenantId(), alarmComment.getId()).isSuccessful(); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.java new file mode 100644 index 0000000000..5c4b868097 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmCommentService.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.service.entitiy.alarm; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbAlarmCommentService { + AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException; + + Boolean deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user); +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index cdd84b0478..3c445b7682 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -32,6 +32,7 @@ public class CustomerUserPermissions extends AbstractPermissions { public CustomerUserPermissions() { super(); put(Resource.ALARM, customerAlarmPermissionChecker); + put(Resource.ALARM_COMMENT, customerAlarmPermissionChecker); put(Resource.ASSET, customerEntityPermissionChecker); put(Resource.DEVICE, customerEntityPermissionChecker); put(Resource.CUSTOMER, customerPermissionChecker); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index a9484a2bd3..0979c0969d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -22,6 +22,7 @@ import java.util.Optional; public enum Resource { ADMIN_SETTINGS(), ALARM(EntityType.ALARM), + ALARM_COMMENT(EntityType.ALARM_COMMENT), DEVICE(EntityType.DEVICE), ASSET(EntityType.ASSET), CUSTOMER(EntityType.CUSTOMER), diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index ef7991e5de..7027fda414 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -30,6 +30,7 @@ public class TenantAdminPermissions extends AbstractPermissions { super(); put(Resource.ADMIN_SETTINGS, PermissionChecker.allowAllPermissionChecker); put(Resource.ALARM, tenantEntityPermissionChecker); + put(Resource.ALARM_COMMENT, tenantEntityPermissionChecker); put(Resource.ASSET, tenantEntityPermissionChecker); put(Resource.DEVICE, tenantEntityPermissionChecker); put(Resource.CUSTOMER, tenantEntityPermissionChecker); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmCommentsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmCommentsCleanUpService.java new file mode 100644 index 0000000000..1c4c2e700b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmCommentsCleanUpService.java @@ -0,0 +1,62 @@ +/** + * 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.ttl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.alarm.AlarmCommentDao; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; +import org.thingsboard.server.queue.discovery.PartitionService; + +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME; + +@Service +@ConditionalOnExpression("${sql.ttl.alarm_comments.enabled:true} && ${sql.ttl.alarm_comments.ttl:0} > 0") +@Slf4j +public class AlarmCommentsCleanUpService extends AbstractCleanUpService { + + private final AlarmCommentDao alarmCommentDao; + private final SqlPartitioningRepository partitioningRepository; + + @Value("${sql.ttl.alarm_comments.ttl:0}") + private long ttlInSec; + @Value("${sql.alarm_comments.partition_size:168}") + private int partitionSizeInHours; + + public AlarmCommentsCleanUpService(PartitionService partitionService, AlarmCommentDao alarmCommentDao, SqlPartitioningRepository partitioningRepository) { + super(partitionService); + this.alarmCommentDao = alarmCommentDao; + this.partitioningRepository = partitioningRepository; + } + + @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.alarm_comments.checking_interval_ms})}", + fixedDelayString = "${sql.ttl.alarm_comments.checking_interval_ms}") + public void cleanUp() { + long commentsExpTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec); + if (isSystemTenantPartitionMine()) { + alarmCommentDao.cleanUpAlarmComments(commentsExpTime); + } else { + partitioningRepository.cleanupPartitionsCache(ALARM_COMMENT_COLUMN_FAMILY_NAME, commentsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); + } + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 47273d03b5..9fdd263a74 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -277,6 +277,8 @@ sql: partition_size: "${SQL_EDGE_EVENTS_PARTITION_SIZE_HOURS:168}" # Number of hours to partition the events. The current value corresponds to one week. audit_logs: partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week + alarm_comments: + partition_size: "${SQL_ALARM_COMMENTS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week # Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks batch_sort: "${SQL_BATCH_SORT:true}" # Specify whether to remove null characters from strValue of attributes and timeseries before insert @@ -312,6 +314,10 @@ sql: alarms: checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # To delete outdated alarms not all at once but in batches + alarms_comments: + enabled: "${SQL_TTL_ALARM_COMMENTS_ENABLED:true}" + ttl: "${SQL_TTL_ALARM_COMMENTS_SECS:0}" # Disabled by default. Accuracy of the cleanup depends on the sql.alarm_comments.partition_size + checking_interval_ms: "${SQL_TTL_ALARM_COMMENTS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day rpc: enabled: "${SQL_TTL_RPC_ENABLED:true}" checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours @@ -582,6 +588,7 @@ audit-log: "user": "${AUDIT_LOG_MASK_USER:W}" "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" "alarm": "${AUDIT_LOG_MASK_ALARM:W}" + "alarm_comment": "${AUDIT_LOG_MASK_ALARM_COMMENT:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 8de7157b52..9653fdba33 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -340,6 +340,12 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.argThat(matcherOriginatorId), Mockito.any(TbMsg.class), Mockito.isNull()); } + protected void testPushMsgToRuleEngineTime(EntityId matcherOriginatorId, TenantId tenantId, HasName entity, int cntTime) { + tenantId = tenantId.isNullUid() && ((HasTenantId) entity).getTenantId() != null ? ((HasTenantId) entity).getTenantId() : tenantId; + Mockito.verify(tbClusterService, times(cntTime)).pushMsgToRuleEngine(Mockito.eq(tenantId), + Mockito.eq(matcherOriginatorId), Mockito.any(TbMsg.class), Mockito.isNull()); + } + private void testNotificationMsgToEdgeServiceTime(EntityId entityId, TenantId tenantId, ActionType actionType, int cntTime) { EdgeEventActionType edgeEventActionType = ActionType.CREDENTIALS_UPDATED.equals(actionType) ? EdgeEventActionType.CREDENTIALS_UPDATED : edgeTypeByActionType(actionType); @@ -367,7 +373,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { Mockito.verify(tbClusterService, times(cntTime)).pushMsgToCore(Mockito.any(ToDeviceActorNotificationMsg.class), Mockito.isNull()); } - private void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId, + protected void testLogEntityAction(HasName entity, EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, int cntTime, Object... additionalInfo) { ArgumentMatcher matcherEntityEquals = entity == null ? Objects::isNull : argument -> argument.toString().equals(entity.toString()); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java new file mode 100644 index 0000000000..087c77b391 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java @@ -0,0 +1,369 @@ +/** + * 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.ContextConfiguration; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.dao.alarm.AlarmDao; + +import java.util.LinkedList; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Slf4j +@ContextConfiguration(classes = {BaseAlarmCommentControllerTest.Config.class}) +public abstract class BaseAlarmCommentControllerTest extends AbstractControllerTest { + + protected Device customerDevice; + protected Alarm alarm; + + static class Config { + @Bean + @Primary + public AlarmDao alarmDao(AlarmDao alarmDao) { + return Mockito.mock(AlarmDao.class, AdditionalAnswers.delegatesTo(alarmDao)); + } + } + + @Before + public void setup() throws Exception { + loginTenantAdmin(); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Test device"); + device.setLabel("Label"); + device.setType("Type"); + device.setCustomerId(customerId); + customerDevice = doPost("/api/device", device, Device.class); + + alarm = Alarm.builder() + .tenantId(tenantId) + .customerId(customerId) + .originator(customerDevice.getId()) + .status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) + .type("test alarm type") + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + resetTokens(); + } + + @After + public void teardown() throws Exception { + Mockito.reset(tbClusterService, auditLogService); + loginSysAdmin(); + deleteDifferentTenant(); + } + + @Test + public void testCreateAlarmCommentViaCustomer() throws Exception { + loginCustomerUser(); + + Mockito.reset(tbClusterService, auditLogService); + + AlarmComment createdComment = createAlarmComment(alarm.getId()); + + testLogEntityAction(createdComment, createdComment.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED, 1); + testPushMsgToRuleEngineTime(createdComment.getId(), tenantId, createdComment, 1); + } + + @Test + public void testCreateAlarmCommentViaTenant() throws Exception { + loginTenantAdmin(); + + Mockito.reset(tbClusterService, auditLogService); + + AlarmComment createdComment = createAlarmComment(alarm.getId()); + Assert.assertEquals("OTHER", createdComment.getType()); + + testLogEntityAction(createdComment, createdComment.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED, 1); + testPushMsgToRuleEngineTime(createdComment.getId(), tenantId, createdComment, 1); + } + + @Test + public void testUpdateAlarmCommentViaCustomer() throws Exception { + loginCustomerUser(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + AlarmComment updatedAlarmComment = saveAlarmComment(alarm.getId(), savedComment); + + Assert.assertNotNull(updatedAlarmComment); + Assert.assertEquals(newComment.get("text"), updatedAlarmComment.getComment().get("text")); + Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); + + testLogEntityAction(updatedAlarmComment, updatedAlarmComment.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED, 1); + testPushMsgToRuleEngineTime(updatedAlarmComment.getId(), tenantId, updatedAlarmComment, 1); + } + + @Test + public void testUpdateAlarmViaTenant() throws Exception { + loginTenantAdmin(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + AlarmComment updatedAlarmComment = saveAlarmComment(alarm.getId(), savedComment); + + Assert.assertNotNull(updatedAlarmComment); + Assert.assertEquals(newComment.get("text"), updatedAlarmComment.getComment().get("text")); + Assert.assertEquals("true", updatedAlarmComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedAlarmComment.getComment().get("editedOn")); + + testLogEntityAction(updatedAlarmComment, updatedAlarmComment.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED, 1); + testPushMsgToRuleEngineTime(updatedAlarmComment.getId(), tenantId, updatedAlarmComment, 1); + } + + @Test + public void testUpdateAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + + doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(savedComment.getId(), savedComment); + } + + @Test + public void testUpdateAlarmViaDifferentCustomer() throws Exception { + loginCustomerUser(); + AlarmComment savedComment = createAlarmComment(alarm.getId()); + + loginDifferentCustomer(); + + Mockito.reset(tbClusterService, auditLogService); + JsonNode newComment = JacksonUtil.newObjectNode().set("text", new TextNode("Updated comment")); + savedComment.setComment(newComment); + + doPost("/api/alarm/" + alarm.getId() + "/comment", savedComment) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(savedComment.getId(), savedComment); + } + + @Test + public void testDeleteAlarmViaCustomer() throws Exception { + loginCustomerUser(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isOk()); + + testNotifyEntityOneTimeMsgToEdgeServiceNever(alarmComment, alarmComment.getId(), alarmComment.getId(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); + } + + @Test + public void testDeleteAlarmViaTenant() throws Exception { + loginTenantAdmin(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isOk()); + + testNotifyEntityOneTimeMsgToEdgeServiceNever(alarmComment, alarmComment.getId(), alarmComment.getId(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); + } + + @Test + public void testDeleteAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), alarm); + } + + @Test + public void testDeleteAlarmViaDifferentCustomer() throws Exception { + loginCustomerUser(); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + loginDifferentCustomer(); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + + testNotifyEntityNever(alarm.getId(), alarm); + } + + @Test + public void testFindAlarmCommentsViaCustomerUser() throws Exception { + loginCustomerUser(); + + List createdAlarmComments = new LinkedList<>(); + + final int size = 10; + for (int i = 0; i < size; i++) { + createdAlarmComments.add( + createAlarmComment(alarm.getId(), RandomStringUtils.randomAlphanumeric(10)) + ); + } + + var response = doGetTyped( + "/api/alarm/" + alarm.getId() + "/comment?page=0&pageSize=" + size, + new TypeReference>() {} + ); + var foundAlarmCommentInfos = response.getData(); + Assert.assertNotNull("Found pageData is null", foundAlarmCommentInfos); + Assert.assertNotEquals( + "Expected alarms are not found!", + 0, foundAlarmCommentInfos.size() + ); + + boolean allMatch = createdAlarmComments.stream() + .allMatch(alarmComment -> foundAlarmCommentInfos.stream() + .map(AlarmCommentInfo::getComment) + .anyMatch(comment -> alarmComment.getComment().equals(comment)) + ); + Assert.assertTrue("Created alarm comment doesn't match any found!", allMatch); + } + + @Test + public void testFindAlarmsViaDifferentCustomerUser() throws Exception { + loginCustomerUser(); + + final int size = 10; + List createdAlarmComments = new LinkedList<>(); + for (int i = 0; i < size; i++) { + createdAlarmComments.add( + createAlarmComment(alarm.getId(), RandomStringUtils.randomAlphanumeric(10)) + ); + } + + loginDifferentCustomer(); + doGet("/api/alarm/" + alarm.getId() + "/comment?page=0&pageSize=" + size) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + } + + @Test + public void testFindAlarmCommentsViaPublicCustomer() throws Exception { + loginTenantAdmin(); + + Device device = new Device(); + device.setName("Test Public Device"); + device.setLabel("Label"); + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + device = doPost("/api/customer/public/device/" + device.getUuidId(), Device.class); + + String publicId = device.getCustomerId().toString(); + + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) + .type("Test") + .build(); + + alarm = doPost("/api/alarm", alarm, Alarm.class); + + Mockito.reset(tbClusterService, auditLogService); + AlarmComment alarmComment = createAlarmComment(alarm.getId()); + + resetTokens(); + + JsonNode publicLoginRequest = JacksonUtil.toJsonNode("{\"publicId\": \"" + publicId + "\"}"); + JsonNode tokens = doPost("/api/auth/login/public", publicLoginRequest, JsonNode.class); + this.token = tokens.get("token").asText(); + + PageData pageData = doGetTyped( + "/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=1", new TypeReference>() {} + ); + + Assert.assertNotNull("Found pageData is null", pageData); + Assert.assertNotEquals("Expected alarms are not found!", 0, pageData.getTotalElements()); + + AlarmCommentInfo alarmCommentInfo = pageData.getData().get(0); + boolean equals = alarmComment.getId().equals(alarmCommentInfo.getId()) && alarmComment.getComment().equals(alarmCommentInfo.getComment()); + Assert.assertTrue("Created alarm doesn't match the found one!", equals); + } + + private AlarmComment createAlarmComment(AlarmId alarmId, String text) { + AlarmComment alarmComment = AlarmComment.builder() + .comment(JacksonUtil.newObjectNode().set("text", new TextNode(text))) + .build(); + + return saveAlarmComment(alarmId, alarmComment); + } + private AlarmComment createAlarmComment(AlarmId alarmId) { + return createAlarmComment(alarmId, "Please take a look"); + } + private AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) { + alarmComment = doPost("/api/alarm/" + alarmId + "/comment", alarmComment, AlarmComment.class); + Assert.assertNotNull(alarmComment); + + return alarmComment; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.java new file mode 100644 index 0000000000..ecf2791c1c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/AlarmCommentControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.controller.sql; + +import org.thingsboard.server.controller.BaseAlarmCommentControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AlarmCommentControllerSqlTest extends BaseAlarmCommentControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index dba6546fc0..0314788065 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; @@ -62,6 +63,8 @@ public class DefaultTbAlarmServiceTest { @MockBean protected AlarmService alarmService; @MockBean + protected AlarmCommentService alarmCommentService; + @MockBean protected AlarmSubscriptionService alarmSubscriptionService; @MockBean protected CustomerService customerService; diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java new file mode 100644 index 0000000000..808af7d2a1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarmComment/DefaultTbAlarmCommentServiceTest.java @@ -0,0 +1,96 @@ +/** + * 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.entitiy.alarmComment; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.dao.alarm.AlarmCommentOperationResult; +import org.thingsboard.server.dao.alarm.AlarmCommentService; +import org.thingsboard.server.dao.alarm.AlarmService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.service.entitiy.TbNotificationEntityService; +import org.thingsboard.server.service.entitiy.alarm.DefaultTbAlarmCommentService; +import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = DefaultTbAlarmCommentService.class) +@TestPropertySource(properties = { + "server.log_controller_error_stack_trace=false" +}) +public class DefaultTbAlarmCommentServiceTest { + + @MockBean + protected DbCallbackExecutorService dbExecutor; + @MockBean + protected TbNotificationEntityService notificationEntityService; + @MockBean + protected AlarmService alarmService; + @MockBean + protected AlarmCommentService alarmCommentService; + @MockBean + protected AlarmSubscriptionService alarmSubscriptionService; + @MockBean + protected CustomerService customerService; + @MockBean + protected TbClusterService tbClusterService; + @SpyBean + DefaultTbAlarmCommentService service; + + @Test + public void testSave() throws ThingsboardException { + var alarm = new Alarm(); + var alarmComment = new AlarmComment(); + when(alarmCommentService.createOrUpdateAlarmComment(Mockito.any(), eq(alarmComment))).thenReturn(new AlarmCommentOperationResult(alarmComment, true)); + service.saveAlarmComment(alarm, alarmComment, new User()); + + verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarmComment(any(), any(), any(), any()); + } + + @Test + public void testDelete() { + var alarmId = new AlarmId(UUID.randomUUID()); + var alarmCommentId = new AlarmCommentId(UUID.randomUUID()); + + when(alarmCommentService.deleteAlarmComment(Mockito.any(), eq(alarmCommentId))).thenReturn(new AlarmCommentOperationResult(new AlarmComment(), true)); + service.deleteAlarmComment(new Alarm(alarmId), new AlarmComment(alarmCommentId), new User()); + + verify(notificationEntityService, times(1)).notifyDeleteAlarmComment(any(), any(), any()); + } +} \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentOperationResult.java new file mode 100644 index 0000000000..3dc25e02ab --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentOperationResult.java @@ -0,0 +1,36 @@ +/** + * 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.dao.alarm; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.AlarmComment; + +@Data +public class AlarmCommentOperationResult { + private final AlarmComment alarmComment; + private final boolean successful; + private final boolean created; + + public AlarmCommentOperationResult(AlarmComment alarmComment, boolean successful) { + this(alarmComment, successful, false); + } + + public AlarmCommentOperationResult(AlarmComment alarmComment, boolean successful, boolean created) { + this.alarmComment = alarmComment; + this.successful = successful; + this.created = created; + } +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java new file mode 100644 index 0000000000..f9274f9a87 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentService.java @@ -0,0 +1,38 @@ +/** + * 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.dao.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface AlarmCommentService { + AlarmCommentOperationResult createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment); + + AlarmCommentOperationResult deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); + + PageData findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink); + + ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId); + + AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 70f8f00ebb..793f020045 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -18,12 +18,12 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 0798647903..44b175c20d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java new file mode 100644 index 0000000000..dae96eb0c2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -0,0 +1,87 @@ +/** + * 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.alarm; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@ApiModel +@Data +@Builder +@AllArgsConstructor +public class AlarmComment extends BaseData implements HasName { + @ApiModelProperty(position = 3, value = "JSON object with Alarm id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private EntityId alarmId; + @ApiModelProperty(position = 4, value = "JSON object with User id.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private UserId userId; + @ApiModelProperty(position = 5, value = "Defines origination of comment", example = "System/Other", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @NoXss + @Length(fieldName = "type") + private String type; + @ApiModelProperty(position = 6, value = "JSON object with text of comment.", dataType = "com.fasterxml.jackson.databind.JsonNode") + private transient JsonNode comment; + + @ApiModelProperty(position = 1, value = "JSON object with the alarm comment Id. " + + "Specify this field to update the alarm comment. " + + "Referencing non-existing alarm Id will cause error. " + + "Omit this field to create new alarm." ) + @Override + public AlarmCommentId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the alarm comment creation, in milliseconds", example = "1634058704567", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + public AlarmComment() { + super(); + } + + public AlarmComment(AlarmCommentId id) { + super(id); + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @ApiModelProperty(position = 5, required = true, value = "representing comment text", example = "Please take a look") + public String getName() { + return comment.toString(); + } + + public AlarmComment(AlarmComment alarmComment) { + super(alarmComment.getId()); + this.createdTime = alarmComment.getCreatedTime(); + this.alarmId = alarmComment.getAlarmId(); + this.type = alarmComment.getType(); + this.comment = alarmComment.getComment(); + this.userId = alarmComment.getUserId(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java new file mode 100644 index 0000000000..66300e4cdb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCommentInfo.java @@ -0,0 +1,52 @@ +/** + * 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.alarm; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmCommentInfo extends AlarmComment { + private static final long serialVersionUID = 2807343093519543377L; + + @ApiModelProperty(position = 19, value = "User first name", example = "John") + private String firstName; + + @ApiModelProperty(position = 19, value = "User last name", example = "Brown") + private String lastName; + + @ApiModelProperty(position = 19, value = "User email address", example = "johnBrown@gmail.com") + private String email; + + public AlarmCommentInfo() { + super(); + } + + public AlarmCommentInfo(AlarmComment alarmComment) { + super(alarmComment); + } + + public AlarmCommentInfo(AlarmComment alarmComment, String firstName, String lastName, String email) { + super(alarmComment); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.java new file mode 100644 index 0000000000..3e0c415b0f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmCommentId.java @@ -0,0 +1,45 @@ +/** + * 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +@ApiModel +public class AlarmCommentId extends UUIDBased implements EntityId{ + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AlarmCommentId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AlarmCommentId fromString(String commentId) { + return new AlarmCommentId(UUID.fromString(commentId)); + } + + @ApiModelProperty(position = 2, required = true, value = "string", example = "ALARM_COMMENT", allowableValues = "ALARM_COMMENT") + @Override + public EntityType getEntityType() { + return EntityType.ALARM_COMMENT; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index c6a19e0040..ae647825cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -53,6 +53,8 @@ public class EntityIdFactory { return new AssetId(uuid); case ALARM: return new AlarmId(uuid); + case ALARM_COMMENT: + return new AlarmCommentId(uuid); case RULE_CHAIN: return new RuleChainId(uuid); case RULE_NODE: diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java new file mode 100644 index 0000000000..aa1d3913cb --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmCommentDao.java @@ -0,0 +1,43 @@ +/** + * 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.dao.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface AlarmCommentDao extends Dao { + + AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment); + + void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); + + AlarmComment findAlarmCommentById(TenantId tenantId, UUID key); + + PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink); + + ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, UUID key); + + void cleanUpAlarmComments(long auditLogsExpTime); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java new file mode 100644 index 0000000000..96ecf8cf96 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java @@ -0,0 +1,127 @@ +/** + * 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.dao.alarm; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.AbstractEntityService; +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.UUID; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class BaseAlarmCommentService extends AbstractEntityService implements AlarmCommentService{ + + @Autowired + private AlarmCommentDao alarmCommentDao; + + @Autowired + private UserService userService; + + @Autowired + private DataValidator alarmCommentDataValidator; + + @Override + public AlarmCommentOperationResult createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) { + alarmCommentDataValidator.validate(alarmComment, tenantId); + if (alarmComment.getId() == null) { + return createAlarmComment(tenantId, alarmComment); + } else { + return updateAlarmComment(tenantId, alarmComment); + } + } + + @Override + public AlarmCommentOperationResult deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.debug("Deleting Alarm Comment with id: {}", alarmCommentId); + AlarmCommentOperationResult result = new AlarmCommentOperationResult(new AlarmComment(), true); + alarmCommentDao.deleteAlarmComment(tenantId, alarmCommentId); + return result; + } + + @Override + public PageData findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink) { + log.trace("Executing findAlarmComments by alarmId [{}]", alarmId); + return alarmCommentDao.findAlarmComments(tenantId, alarmId, pageLink); + } + + @Override + public ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + return alarmCommentDao.findAlarmCommentByIdAsync(tenantId, alarmCommentId.getId()); + } + + @Override + public AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId) { + log.trace("Executing findAlarmCommentByIdAsync by alarmCommentId [{}]", alarmCommentId); + validateId(alarmCommentId, "Incorrect alarmCommentId " + alarmCommentId); + return alarmCommentDao.findById(tenantId, alarmCommentId.getId()); + } + + private AlarmCommentOperationResult createAlarmComment(TenantId tenantId, AlarmComment alarmComment) { + log.debug("New Alarm comment : {}", alarmComment); + if (alarmComment.getType() == null) { + alarmComment.setType("OTHER"); + } + if (alarmComment.getId() == null) { + UUID uuid = Uuids.timeBased(); + alarmComment.setId(new AlarmCommentId(uuid)); + alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid)); + } + AlarmComment saved = alarmCommentDao.createAlarmComment(tenantId, alarmComment); + return new AlarmCommentOperationResult(saved, true, true); + } + + private AlarmCommentOperationResult updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) { + log.debug("Update Alarm comment : {}", newAlarmComment); + + AlarmComment existing = alarmCommentDao.findAlarmCommentById(tenantId, newAlarmComment.getId().getId()); + if (existing != null) { + if (newAlarmComment.getComment() != null) { + JsonNode comment = newAlarmComment.getComment(); + UUID uuid = Uuids.timeBased(); + ((ObjectNode) comment).put("edited", "true"); + ((ObjectNode) comment).put("editedOn", Uuids.unixTimestamp(uuid)); + existing.setComment(comment); + } + AlarmComment result = alarmCommentDao.save(tenantId, existing); + return new AlarmCommentOperationResult(result, true, true); + } + return null; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e24bde91fb..fc5c663d7c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -297,6 +297,14 @@ public class ModelConstants { public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; + public static final String ALARM_COMMENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String ALARM_COMMENT_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String ALARM_COMMENT_COLUMN_FAMILY_NAME = "alarm_comment"; + public static final String ALARM_COMMENT_ALARM_ID = "alarm_id"; + public static final String ALARM_COMMENT_USER_ID = USER_ID_PROPERTY; + public static final String ALARM_COMMENT_TYPE = "type"; + public static final String ALARM_COMMENT_COMMENT = "comment"; + /** * Cassandra entity relation constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java new file mode 100644 index 0000000000..c8651b86c8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmCommentEntity.java @@ -0,0 +1,99 @@ +/** + * 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.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_ALARM_ID; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COMMENT; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_TYPE; + +@Data +@EqualsAndHashCode(callSuper = true) +@TypeDef(name = "json", typeClass = JsonStringType.class) +@MappedSuperclass +public abstract class AbstractAlarmCommentEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = ALARM_COMMENT_ALARM_ID, columnDefinition = "uuid") + private UUID alarmId; + + @Column(name = ModelConstants.ALARM_COMMENT_USER_ID) + private UUID userId; + + @Column(name = ALARM_COMMENT_TYPE) + private String type; + + @Type(type = "json") + @Column(name = ALARM_COMMENT_COMMENT) + private JsonNode comment; + + public AbstractAlarmCommentEntity() { + super(); + } + + public AbstractAlarmCommentEntity(AlarmComment alarmComment) { + if (alarmComment.getId() != null) { + this.setUuid(alarmComment.getUuidId()); + } + this.setCreatedTime(alarmComment.getCreatedTime()); + if (alarmComment.getAlarmId() != null) { + this.alarmId = alarmComment.getAlarmId().getId(); + } + if (alarmComment.getUserId() != null) { + this.userId = alarmComment.getUserId().getId(); + } + if (alarmComment.getType() != null) { + this.type = alarmComment.getType(); + } + this.setComment(alarmComment.getComment()); + } + + public AbstractAlarmCommentEntity(AlarmCommentEntity alarmCommentEntity) { + this.setId(alarmCommentEntity.getId()); + this.setCreatedTime(alarmCommentEntity.getCreatedTime()); + this.userId = alarmCommentEntity.getUserId(); + this.alarmId = alarmCommentEntity.getAlarmId(); + this.type = alarmCommentEntity.getType(); + this.comment = alarmCommentEntity.getComment(); + } + protected AlarmComment toAlarmComment() { + AlarmComment alarmComment = new AlarmComment(new AlarmCommentId(id)); + alarmComment.setCreatedTime(createdTime); + alarmComment.setAlarmId(new AlarmId(alarmId)); + if (userId != null) { + alarmComment.setUserId(new UserId(userId)); + } + alarmComment.setType(type); + alarmComment.setComment(comment); + return alarmComment; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.java new file mode 100644 index 0000000000..6f9a507cbd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentEntity.java @@ -0,0 +1,55 @@ +/** + * 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.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Entity; +import javax.persistence.Table; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COLUMN_FAMILY_NAME; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ALARM_COMMENT_COLUMN_FAMILY_NAME) + +public class AlarmCommentEntity extends AbstractAlarmCommentEntity { + + public AlarmCommentEntity() { + super(); + } + + public AlarmCommentEntity(AlarmCommentInfo alarmCommentInfo) { + super(alarmCommentInfo); + } + + public AlarmCommentEntity(AlarmComment alarmComment) { + super(alarmComment); + } + + @Override + public AlarmComment toData() { + return super.toAlarmComment(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.java new file mode 100644 index 0000000000..bc9c5841c5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmCommentInfoEntity.java @@ -0,0 +1,53 @@ +/** + * 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.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; + +import java.util.HashMap; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class AlarmCommentInfoEntity extends AbstractAlarmCommentEntity { + + private String firstName; + private String lastName; + + private String email; + + public AlarmCommentInfoEntity() { + super(); + } + + public AlarmCommentInfoEntity(AlarmCommentEntity alarmCommentEntity) { + super(alarmCommentEntity); + } + + public AlarmCommentInfoEntity(AlarmCommentEntity alarmCommentEntity, String firstName, String lastName, String email) { + super(alarmCommentEntity); + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + } + + @Override + public AlarmCommentInfo toData() { + return new AlarmCommentInfo(super.toAlarmComment(), this.firstName, this.lastName, this.email); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index beb4a7ebc1..6efe89df4f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -67,6 +67,29 @@ public abstract class DataValidator> { } } + public D validate(D data, TenantId tenantId) { + try { + if (data == null) { + throw new DataValidationException("Data object can't be null!"); + } + + ConstraintValidator.validateFields(data); + + validateDataImpl(tenantId, data); + D old; + if (data.getId() == null) { + validateCreate(tenantId, data); + old = null; + } else { + old = validateUpdate(tenantId, data); + } + return old; + } catch (DataValidationException e) { + log.error("{} object is invalid: [{}]", data == null ? "Data" : data.getClass().getSimpleName(), e.getMessage()); + throw e; + } + } + protected void validateDataImpl(TenantId tenantId, D data) { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java new file mode 100644 index 0000000000..d85a58bc6d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java @@ -0,0 +1,38 @@ +/** + * 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.dao.service.validator; + +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +@Component +@AllArgsConstructor +public class AlarmCommentDataValidator extends DataValidator { + + @Override + protected void validateDataImpl(TenantId tenantId, AlarmComment alarmComment) { + if (alarmComment.getComment() == null) { + throw new DataValidationException("Alarm comment should be specified!"); + } + if (alarmComment.getAlarmId() == null) { + throw new DataValidationException("Alarm id should be specified!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java new file mode 100644 index 0000000000..19dfd6ec2e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmCommentRepository.java @@ -0,0 +1,39 @@ +/** + * 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.dao.sql.alarm; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; +import org.thingsboard.server.dao.model.sql.AlarmCommentInfoEntity; + +import java.util.UUID; + +public interface AlarmCommentRepository extends JpaRepository { + + @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmCommentInfoEntity(a, u.firstName, u.lastName, u.email) FROM AlarmCommentEntity a " + + "LEFT JOIN UserEntity u on u.id = a.userId " + + "WHERE a.alarmId = :alarmId ", + countQuery = "" + + "SELECT count(a) " + + "FROM AlarmCommentEntity a " + + "WHERE a.alarmId = :alarmId ") + Page findAllByAlarmId(@Param("alarmId") UUID alarmId, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java new file mode 100644 index 0000000000..5691f7a861 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java @@ -0,0 +1,106 @@ +/** + * 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.dao.sql.alarm; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.alarm.AlarmCommentDao; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.sql.AlarmCommentEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_COMMENT_COLUMN_FAMILY_NAME; + +@Slf4j +@Component +@SqlDao +@RequiredArgsConstructor +public class JpaAlarmCommentDao extends JpaAbstractDao implements AlarmCommentDao { + private final SqlPartitioningRepository partitioningRepository; + @Value("${sql.alarm_comments.partition_size:168}") + private int partitionSizeInHours; + + @Autowired + private AlarmCommentRepository alarmCommentRepository; + + private static final String TABLE_NAME = ALARM_COMMENT_COLUMN_FAMILY_NAME; + + @Override + public AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment){ + log.trace("Saving entity {}", alarmComment); + partitioningRepository.createPartitionIfNotExists(ALARM_COMMENT_COLUMN_FAMILY_NAME, alarmComment.getCreatedTime(), TimeUnit.HOURS.toMillis(partitionSizeInHours)); + AlarmCommentEntity saved = alarmCommentRepository.save(new AlarmCommentEntity(alarmComment)); + return DaoUtil.getData(saved); + } + + @Override + public void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId){ + log.trace("Try to delete entity alarm comment by id using [{}]", alarmCommentId); + alarmCommentRepository.deleteById(alarmCommentId.getId()); + } + + @Override + public PageData findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink){ + log.trace("Try to find alarm comments by alarm id using [{}]", id); + return DaoUtil.toPageData( + alarmCommentRepository.findAllByAlarmId(id.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public AlarmComment findAlarmCommentById(TenantId tenantId, UUID key) { + log.trace("Try to find alarm comment by id using [{}]", key); + return DaoUtil.getData(alarmCommentRepository.findById(key)); + } + + @Override + public ListenableFuture findAlarmCommentByIdAsync(TenantId tenantId, UUID key) { + log.trace("Try to find alarm comment by id using [{}]", key); + return findByIdAsync(tenantId, key); + } + + @Override + public void cleanUpAlarmComments(long expTime) { + partitioningRepository.dropPartitionsBefore(TABLE_NAME, expTime, TimeUnit.HOURS.toMillis(partitionSizeInHours)); + } + + @Override + protected Class getEntityClass() { + return AlarmCommentEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return alarmCommentRepository; + } +} diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index e4c766e81d..b135e60b8e 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -79,3 +79,5 @@ CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); + +CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id ASC); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 73039274a6..290e330630 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -776,3 +776,13 @@ CREATE TABLE IF NOT EXISTS user_auth_settings ( user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id), two_fa_settings varchar ); + +CREATE TABLE IF NOT EXISTS alarm_comment ( + id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_id uuid NOT NULL, + user_id uuid, + type varchar(255) NOT NULL, + comment varchar(1000000), + CONSTRAINT fk_alarm_comment_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +) PARTITION BY RANGE (created_time); \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 5ea3874d19..61147120bc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -149,6 +150,8 @@ public abstract class AbstractServiceTest { @Autowired protected AlarmService alarmService; + @Autowired + protected AlarmCommentService alarmCommentService; @Autowired protected RuleChainService ruleChainService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java new file mode 100644 index 0000000000..7d9b258ba6 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java @@ -0,0 +1,162 @@ +/** + * 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.dao.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +public abstract class BaseAlarmCommentServiceTest extends AbstractServiceTest { + + public static final String TEST_ALARM = "TEST_ALARM"; + private TenantId tenantId; + private Alarm alarm; + private User user; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + + alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased())) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .startTs(System.currentTimeMillis()).build(); + alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm(); + + user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail("tenant@thingsboard.org"); + user.setFirstName("John"); + user.setLastName("Brown"); + user = userService.saveUser(user); + } + + @After + public void after() { + alarmService.deleteAlarm(tenantId, alarm.getId()); + tenantService.deleteTenant(tenantId); + } + + + @Test + public void testSaveAndFetchAlarmComment() throws ExecutionException, InterruptedException { + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(user.getId()) + .type("OTHER") + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment).getAlarmComment(); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + Assert.assertEquals(alarm.getId(), createdComment.getAlarmId()); + Assert.assertEquals(user.getId(), createdComment.getUserId()); + Assert.assertEquals("OTHER", createdComment.getType()); + Assert.assertTrue(createdComment.getCreatedTime() > 0); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + Assert.assertEquals(createdComment, fetched); + + PageData alarmComments = alarmCommentService.findAlarmComments(tenantId, alarm.getId(), new PageLink(10, 0)); + Assert.assertNotNull(alarmComments.getData()); + Assert.assertEquals(1, alarmComments.getData().size()); + Assert.assertEquals(createdComment, new AlarmComment(alarmComments.getData().get(0))); + } + + @Test + public void testUpdateAlarmComment() throws ExecutionException, InterruptedException { + UserId userId = new UserId(UUID.randomUUID()); + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(userId) + .type("OTHER") + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment).getAlarmComment(); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + //update comment + String newComment = "new comment"; + createdComment.setComment(JacksonUtil.newObjectNode().put("text", newComment)); + AlarmComment updatedComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, createdComment).getAlarmComment(); + + Assert.assertEquals(alarm.getId(), updatedComment.getAlarmId()); + Assert.assertEquals(userId, updatedComment.getUserId()); + Assert.assertEquals("OTHER", updatedComment.getType()); + Assert.assertTrue(updatedComment.getCreatedTime() > 0); + Assert.assertEquals(newComment, updatedComment.getComment().get("text").asText()); + Assert.assertEquals("true", updatedComment.getComment().get("edited").asText()); + Assert.assertNotNull(updatedComment.getComment().get("editedOn").asText()); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + Assert.assertEquals(updatedComment, fetched); + + PageData alarmComments = alarmCommentService.findAlarmComments(tenantId, alarm.getId(), new PageLink(10, 0)); + Assert.assertNotNull(alarmComments.getData()); + Assert.assertEquals(1, alarmComments.getData().size()); + Assert.assertEquals(updatedComment, new AlarmComment(alarmComments.getData().get(0))); + } + + @Test + public void testDeleteAlarmComment() throws ExecutionException, InterruptedException { + UserId userId = new UserId(UUID.randomUUID()); + AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId()) + .userId(userId) + .type("OTHER") + .comment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))) + .build(); + + AlarmComment createdComment = alarmCommentService.createOrUpdateAlarmComment(tenantId, alarmComment).getAlarmComment(); + + Assert.assertNotNull(createdComment); + Assert.assertNotNull(createdComment.getId()); + + Assert.assertTrue("Alarm comment was not deleted when expected", alarmCommentService.deleteAlarmComment(tenantId, createdComment.getId()).isSuccessful()); + + AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); + + Assert.assertNull("Alarm comment was returned when it was expected to be null", fetched); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.java new file mode 100644 index 0000000000..510b8659e9 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AlarmCommentServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * 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.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseAlarmCommentServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AlarmCommentServiceSqlTest extends BaseAlarmCommentServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java new file mode 100644 index 0000000000..8530525a63 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java @@ -0,0 +1,90 @@ +/** + * 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.dao.sql.alarm; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.AlarmCommentId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmCommentDao; +import org.thingsboard.server.dao.alarm.AlarmDao; + +import java.util.UUID; +import static org.junit.Assert.assertEquals; + +@Slf4j +public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest { + + @Autowired + private AlarmCommentDao alarmCommentDao; + @Autowired + private AlarmDao alarmDao; + + + @Test + public void testFindAlarmCommentsByAlarmId() { + log.info("Current system time in millis = {}", System.currentTimeMillis()); + UUID tenantId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + UUID alarmId1 = UUID.randomUUID(); + UUID alarmId2 = UUID.randomUUID(); + UUID commentId1 = UUID.randomUUID(); + UUID commentId2 = UUID.randomUUID(); + UUID commentId3 = UUID.randomUUID(); + saveAlarm(alarmId1, UUID.randomUUID(), UUID.randomUUID(), "TEST_ALARM"); + saveAlarm(alarmId2, UUID.randomUUID(), UUID.randomUUID(), "TEST_ALARM"); + + saveAlarmComment(commentId1, alarmId1, userId, "OTHER"); + saveAlarmComment(commentId2, alarmId1, userId, "OTHER"); + saveAlarmComment(commentId3, alarmId2, userId, "OTHER"); + + int count = alarmCommentDao.findAlarmComments(TenantId.fromUUID(tenantId), new AlarmId(alarmId1), new PageLink(10, 0)).getData().size(); + assertEquals(2, count); + } + + private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { + Alarm alarm = new Alarm(); + alarm.setId(new AlarmId(id)); + alarm.setTenantId(TenantId.fromUUID(tenantId)); + alarm.setOriginator(new DeviceId(deviceId)); + alarm.setType(type); + alarm.setPropagate(true); + alarm.setStartTs(System.currentTimeMillis()); + alarm.setEndTs(System.currentTimeMillis()); + alarm.setStatus(AlarmStatus.ACTIVE_UNACK); + alarmDao.save(TenantId.fromUUID(tenantId), alarm); + } + private void saveAlarmComment(UUID id, UUID alarmId, UUID userId, String type) { + AlarmComment alarmComment = new AlarmComment(); + alarmComment.setId(new AlarmCommentId(id)); + alarmComment.setAlarmId(TenantId.fromUUID(alarmId)); + alarmComment.setUserId(new UserId(userId)); + alarmComment.setType(type); + alarmComment.setComment(JacksonUtil.newObjectNode().put("text", RandomStringUtils.randomAlphanumeric(10))); + alarmCommentDao.createAlarmComment(TenantId.fromUUID(UUID.randomUUID()), alarmComment); + } +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1786fd08be..332887d081 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; @@ -237,6 +238,8 @@ public interface TbContext { RuleEngineAlarmService getAlarmService(); + AlarmCommentService getAlarmCommentService(); + RuleChainService getRuleChainService(); RuleEngineRpcService getRpcService(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java index 245b671cf5..558e197064 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityFieldsData; +import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -56,6 +57,9 @@ public class EntitiesFieldsAsyncLoader { case ALARM: return getAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original), EntityFieldsData::new); + case ALARM_COMMENT: + return getAsync(ctx.getAlarmCommentService().findAlarmCommentByIdAsync(ctx.getTenantId(), (AlarmCommentId) original), + EntityFieldsData::new); case RULE_CHAIN: return getAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) original), EntityFieldsData::new); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index b687246789..344e35084d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -18,6 +18,8 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.alarm.AlarmComment; +import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; @@ -69,6 +71,10 @@ public class TenantIdLoader { case ALARM: tenantEntity = ctx.getAlarmService().findAlarmById(ctxTenantId, new AlarmId(id)); break; + case ALARM_COMMENT: + AlarmComment alarmComment = ctx.getAlarmCommentService().findAlarmCommentById(ctxTenantId, new AlarmCommentId(id)); + tenantEntity = ctx.getAlarmService().findAlarmById(ctxTenantId, new AlarmId(alarmComment.getAlarmId().getId())); + break; case RULE_CHAIN: tenantEntity = ctx.getRuleChainService().findRuleChainById(ctxTenantId, new RuleChainId(id)); break; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 8a53148204..4990439e60 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -41,9 +41,11 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -56,6 +58,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -93,6 +96,8 @@ public class TenantIdLoaderTest { @Mock private RuleEngineAlarmService alarmService; @Mock + private AlarmCommentService alarmCommentService; + @Mock private RuleChainService ruleChainService; @Mock private EntityViewService entityViewService; @@ -190,6 +195,14 @@ public class TenantIdLoaderTest { when(ctx.getAlarmService()).thenReturn(alarmService); doReturn(alarm).when(alarmService).findAlarmById(eq(tenantId), any()); + break; + case ALARM_COMMENT: + AlarmComment alarmComment = new AlarmComment(); + alarmComment.setAlarmId(new AlarmId(UUID.randomUUID())); + + when(ctx.getAlarmCommentService()).thenReturn(alarmCommentService); + doReturn(alarmComment).when(alarmCommentService).findAlarmCommentById(eq(tenantId), any()); + break; case RULE_CHAIN: RuleChain ruleChain = new RuleChain();