114 changed files with 3666 additions and 159 deletions
@ -0,0 +1,107 @@ |
|||
## Install ThingsBoard Edge and connect to cloud instructions |
|||
|
|||
Here is the list of commands, that can be used to quickly install and connect ThingsBoard Edge to the cloud using docker compose. |
|||
|
|||
### Prerequisites |
|||
|
|||
Install <a href="https://docs.docker.com/engine/install/" target="_blank"> Docker CE</a> and <a href="https://docs.docker.com/compose/install/" target="_blank"> Docker Compose</a>. |
|||
|
|||
### Create data and logs folders |
|||
|
|||
Run following commands, before starting docker container(s), to create folders for storing data and logs. |
|||
These commands additionally will change owner of newly created folders to docker container user. |
|||
To do this (to change user) **chown** command is used, and this command requires *sudo* permissions (command will request password for a *sudo* access): |
|||
|
|||
```bash |
|||
mkdir -p ~/.mytb-edge-data && sudo chown -R 799:799 ~/.mytb-edge-data |
|||
mkdir -p ~/.mytb-edge-logs && sudo chown -R 799:799 ~/.mytb-edge-logs |
|||
{:copy-code} |
|||
``` |
|||
|
|||
### Running ThingsBoard Edge as docker service |
|||
|
|||
${LOCALHOST_WARNING} |
|||
|
|||
Create docker compose file for ThingsBoard Edge service: |
|||
|
|||
```bash |
|||
nano docker-compose.yml |
|||
{:copy-code} |
|||
``` |
|||
|
|||
Add the following lines to the yml file: |
|||
|
|||
```bash |
|||
version: '3.0' |
|||
services: |
|||
mytbedge: |
|||
restart: always |
|||
image: "thingsboard/tb-edge:${TB_EDGE_VERSION}" |
|||
ports: |
|||
- "8080:8080" |
|||
- "1883:1883" |
|||
- "5683-5688:5683-5688/udp" |
|||
environment: |
|||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tb-edge |
|||
CLOUD_ROUTING_KEY: ${CLOUD_ROUTING_KEY} |
|||
CLOUD_ROUTING_SECRET: ${CLOUD_ROUTING_SECRET} |
|||
CLOUD_RPC_HOST: ${BASE_URL} |
|||
CLOUD_RPC_PORT: ${CLOUD_RPC_PORT} |
|||
CLOUD_RPC_SSL_ENABLED: ${CLOUD_RPC_SSL_ENABLED} |
|||
volumes: |
|||
- ~/.mytb-edge-data:/data |
|||
- ~/.mytb-edge-logs:/var/log/tb-edge |
|||
postgres: |
|||
restart: always |
|||
image: "postgres:12" |
|||
ports: |
|||
- "5432" |
|||
environment: |
|||
POSTGRES_DB: tb-edge |
|||
POSTGRES_PASSWORD: postgres |
|||
volumes: |
|||
- ~/.mytb-edge-data/db:/var/lib/postgresql/data |
|||
{:copy-code} |
|||
``` |
|||
|
|||
#### [Optional] Update bind ports |
|||
If ThingsBoard Edge is going to be running on the same machine where ThingsBoard server (cloud) is running, you'll need to update docker compose port mapping to avoid port collision between ThingsBoard server and ThingsBoard Edge. |
|||
|
|||
Please update next lines of `docker-compose.yml` file: |
|||
|
|||
```bash |
|||
ports: |
|||
- "18080:8080" |
|||
- "11883:1883" |
|||
- "15683-15688:5683-5688/udp" |
|||
``` |
|||
Make sure that ports above (18080, 11883, 15683-15688) are not used by any other application. |
|||
|
|||
#### Start ThingsBoard Edge |
|||
Set the terminal in the directory which contains the `docker-compose.yml` file and execute the following commands to up this docker compose directly: |
|||
|
|||
```bash |
|||
docker compose up -d |
|||
docker compose logs -f mytbedge |
|||
{:copy-code} |
|||
``` |
|||
|
|||
###### NOTE: Docker Compose V2 vs docker-compose (with a hyphen) |
|||
|
|||
ThingsBoard supports Docker Compose V2 (Docker Desktop or Compose plugin) starting from **3.4.2** release, because **docker-compose** as standalone setup is no longer supported by Docker. |
|||
We **strongly** recommend to update to Docker Compose V2 and use it. |
|||
If you still rely on using Docker Compose as docker-compose (with a hyphen), then please execute the following commands to start ThingsBoard Edge: |
|||
|
|||
```bash |
|||
docker-compose up -d |
|||
docker-compose logs -f mytbedge |
|||
``` |
|||
|
|||
#### Open ThingsBoard Edge UI |
|||
|
|||
Once started, you will be able to open **ThingsBoard Edge UI** using the following link http://localhost:8080. |
|||
|
|||
###### NOTE: Edge HTTP bind port update |
|||
|
|||
Use next **ThingsBoard Edge UI** link **http://localhost:18080** if you updated HTTP 8080 bind port to **18080**. |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
###### WARNING NOTE: 'localhost' can not be used as CLOUD_RPC_HOST |
|||
|
|||
Please note that your ThingsBoard base URL is **'localhost'** at the moment. **'localhost'** cannot be used for docker containers - please update **CLOUD_RPC_HOST** environment variable below to the IP address of your machine (*docker **host** machine*). IP address must be `192.168.1.XX` or similar format. In other case - ThingsBoard Edge service, that is running in docker container, will not be able to connect to the cloud. |
|||
@ -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(10000), |
|||
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); |
|||
@ -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.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 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.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 void 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, alarmId); |
|||
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<AlarmCommentInfo> 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)); |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
/** |
|||
* 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.edge.instructions; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.edge.Edge; |
|||
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.install.InstallScripts; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.io.IOException; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") |
|||
@TbCoreComponent |
|||
public class DefaultEdgeInstallService implements EdgeInstallService { |
|||
|
|||
private static final String EDGE_DIR = "edge"; |
|||
|
|||
private static final String EDGE_INSTALL_INSTRUCTIONS_DIR = "install_instructions"; |
|||
|
|||
private final InstallScripts installScripts; |
|||
|
|||
@Value("${edges.rpc.port}") |
|||
private int rpcPort; |
|||
|
|||
@Value("${edges.rpc.ssl.enabled}") |
|||
private boolean sslEnabled; |
|||
|
|||
@Value("${app.version:unknown}") |
|||
private String appVersion; |
|||
|
|||
@Override |
|||
public EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request) { |
|||
String dockerInstallInstructions = readFile(resolveFile("docker", "instructions.md")); |
|||
String baseUrl = request.getServerName(); |
|||
if (baseUrl.contains("localhost") || baseUrl.contains("127.0.0.1")) { |
|||
String localhostWarning = readFile(resolveFile("docker", "localhost_warning.md")); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", localhostWarning); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", "!!!REPLACE_ME_TO_HOST_IP_ADDRESS!!!"); |
|||
} else { |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${LOCALHOST_WARNING}", ""); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${BASE_URL}", baseUrl); |
|||
} |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${TB_EDGE_VERSION}", appVersion + "EDGE"); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_KEY}", edge.getRoutingKey()); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_ROUTING_SECRET}", edge.getSecret()); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_PORT}", Integer.toString(rpcPort)); |
|||
dockerInstallInstructions = dockerInstallInstructions.replace("${CLOUD_RPC_SSL_ENABLED}", Boolean.toString(sslEnabled)); |
|||
return new EdgeInstallInstructions(dockerInstallInstructions); |
|||
} |
|||
|
|||
private String readFile(Path file) { |
|||
try { |
|||
return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); |
|||
} catch (IOException e) { |
|||
log.warn("Failed to read file: {}", file, e); |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
|
|||
private Path resolveFile(String subDir, String... subDirs) { |
|||
return getEdgeInstallInstructionsDir().resolve(Paths.get(subDir, subDirs)); |
|||
} |
|||
|
|||
private Path getEdgeInstallInstructionsDir() { |
|||
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, EDGE_INSTALL_INSTRUCTIONS_DIR); |
|||
} |
|||
} |
|||
@ -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.edge.instructions; |
|||
|
|||
import org.thingsboard.server.common.data.edge.Edge; |
|||
import org.thingsboard.server.common.data.edge.EdgeInstallInstructions; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
public interface EdgeInstallService { |
|||
|
|||
EdgeInstallInstructions getDockerInstallInstructions(TenantId tenantId, Edge edge, HttpServletRequest request); |
|||
|
|||
} |
|||
@ -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_COMMENT : ActionType.UPDATED_COMMENT; |
|||
UserId userId = user.getId(); |
|||
alarmComment.setUserId(userId); |
|||
try { |
|||
AlarmComment savedAlarmComment = checkNotNull(alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment)); |
|||
notificationEntityService.notifyAlarmComment(alarm, savedAlarmComment, actionType, user); |
|||
return savedAlarmComment; |
|||
} catch (Exception e) { |
|||
notificationEntityService.logEntityAction(alarm.getTenantId(), emptyId(EntityType.ALARM), alarm, actionType, user, e, alarmComment); |
|||
throw e; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) { |
|||
alarmCommentService.deleteAlarmComment(alarm.getTenantId(), alarmComment.getId()); |
|||
notificationEntityService.notifyAlarmComment(alarm, alarmComment, ActionType.DELETED_COMMENT, user); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
public interface TbAlarmCommentService { |
|||
AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException; |
|||
|
|||
void deleteAlarmComment(Alarm alarm, AlarmComment alarmComment, User user); |
|||
} |
|||
@ -0,0 +1,363 @@ |
|||
/** |
|||
* 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.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.AlarmCommentType; |
|||
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(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); |
|||
} |
|||
|
|||
@Test |
|||
public void testCreateAlarmCommentViaTenant() throws Exception { |
|||
loginTenantAdmin(); |
|||
|
|||
Mockito.reset(tbClusterService, auditLogService); |
|||
|
|||
AlarmComment createdComment = createAlarmComment(alarm.getId()); |
|||
Assert.assertEquals(AlarmCommentType.OTHER, createdComment.getType()); |
|||
|
|||
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ADDED_COMMENT, 1, createdComment); |
|||
} |
|||
|
|||
@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(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED_COMMENT, 1, savedComment); |
|||
} |
|||
|
|||
@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(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED_COMMENT, 1, updatedAlarmComment); |
|||
} |
|||
|
|||
@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(alarm.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(alarm.getId(), savedComment); |
|||
} |
|||
|
|||
@Test |
|||
public void testDeleteAlarmСommentViaCustomer() throws Exception { |
|||
loginCustomerUser(); |
|||
AlarmComment alarmComment = createAlarmComment(alarm.getId()); |
|||
|
|||
Mockito.reset(tbClusterService, auditLogService); |
|||
|
|||
doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId()) |
|||
.andExpect(status().isOk()); |
|||
|
|||
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED_COMMENT, 1, alarmComment); |
|||
} |
|||
|
|||
@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()); |
|||
|
|||
testLogEntityAction(alarm, alarm.getId(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED_COMMENT, 1, alarmComment); |
|||
} |
|||
|
|||
@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<AlarmComment> 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<PageData<AlarmCommentInfo>>() {} |
|||
); |
|||
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<AlarmComment> 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<AlarmCommentInfo> pageData = doGetTyped( |
|||
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=1", new TypeReference<PageData<AlarmCommentInfo>>() {} |
|||
); |
|||
|
|||
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; |
|||
} |
|||
} |
|||
@ -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 { |
|||
} |
|||
@ -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.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.doNothing; |
|||
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(alarmComment); |
|||
service.saveAlarmComment(alarm, alarmComment, new User()); |
|||
|
|||
verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); |
|||
} |
|||
|
|||
@Test |
|||
public void testDelete() { |
|||
var alarmId = new AlarmId(UUID.randomUUID()); |
|||
var alarmCommentId = new AlarmCommentId(UUID.randomUUID()); |
|||
|
|||
doNothing().when(alarmCommentService).deleteAlarmComment(Mockito.any(), eq(alarmCommentId)); |
|||
service.deleteAlarmComment(new Alarm(alarmId), new AlarmComment(alarmCommentId), new User()); |
|||
|
|||
verify(notificationEntityService, times(1)).notifyAlarmComment(any(), any(), any(), any()); |
|||
} |
|||
} |
|||
@ -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 { |
|||
AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment); |
|||
|
|||
void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); |
|||
|
|||
PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink); |
|||
|
|||
ListenableFuture<AlarmComment> findAlarmCommentByIdAsync(TenantId tenantId, AlarmCommentId alarmCommentId); |
|||
|
|||
AlarmComment findAlarmCommentById(TenantId tenantId, AlarmCommentId alarmCommentId); |
|||
|
|||
} |
|||
@ -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<AlarmCommentId> 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. System type means comment was created by TB. OTHER type means comment was created by user.", example = "SYSTEM/OTHER", accessMode = ApiModelProperty.AccessMode.READ_ONLY) |
|||
private AlarmCommentType type; |
|||
@ApiModelProperty(position = 6, value = "JSON object with text of comment.", dataType = "com.fasterxml.jackson.databind.JsonNode") |
|||
@NoXss |
|||
@Length(fieldName = "comment", max = 10000) |
|||
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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
public enum AlarmCommentType { |
|||
|
|||
SYSTEM, OTHER; |
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.edge; |
|||
|
|||
import io.swagger.annotations.ApiModel; |
|||
import io.swagger.annotations.ApiModelProperty; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
@ApiModel |
|||
@Data |
|||
@AllArgsConstructor |
|||
@NoArgsConstructor |
|||
public class EdgeInstallInstructions { |
|||
|
|||
@ApiModelProperty(position = 1, value = "Markdown with docker install instructions") |
|||
private String dockerInstallInstructions; |
|||
} |
|||
@ -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.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 { |
|||
|
|||
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)); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
/** |
|||
* 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> { |
|||
|
|||
AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment); |
|||
|
|||
void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId); |
|||
|
|||
AlarmComment findAlarmCommentById(TenantId tenantId, UUID key); |
|||
|
|||
PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId id, PageLink pageLink); |
|||
|
|||
ListenableFuture<AlarmComment> findAlarmCommentByIdAsync(TenantId tenantId, UUID key); |
|||
} |
|||
@ -0,0 +1,115 @@ |
|||
/** |
|||
* 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.ListenableFuture; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.alarm.AlarmComment; |
|||
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; |
|||
import org.thingsboard.server.common.data.alarm.AlarmCommentType; |
|||
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 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 DataValidator<AlarmComment> alarmCommentDataValidator; |
|||
|
|||
@Override |
|||
public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) { |
|||
alarmCommentDataValidator.validate(alarmComment, c -> tenantId); |
|||
if (alarmComment.getId() == null) { |
|||
return createAlarmComment(tenantId, alarmComment); |
|||
} else { |
|||
return updateAlarmComment(tenantId, alarmComment); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void deleteAlarmComment(TenantId tenantId, AlarmCommentId alarmCommentId) { |
|||
log.debug("Deleting Alarm Comment with id: {}", alarmCommentId); |
|||
alarmCommentDao.deleteAlarmComment(tenantId, alarmCommentId); |
|||
} |
|||
|
|||
@Override |
|||
public PageData<AlarmCommentInfo> findAlarmComments(TenantId tenantId, AlarmId alarmId, PageLink pageLink) { |
|||
log.trace("Executing findAlarmComments by alarmId [{}]", alarmId); |
|||
return alarmCommentDao.findAlarmComments(tenantId, alarmId, pageLink); |
|||
} |
|||
|
|||
@Override |
|||
public ListenableFuture<AlarmComment> 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 AlarmComment createAlarmComment(TenantId tenantId, AlarmComment alarmComment) { |
|||
log.debug("New Alarm comment : {}", alarmComment); |
|||
if (alarmComment.getType() == null) { |
|||
alarmComment.setType(AlarmCommentType.OTHER); |
|||
} |
|||
if (alarmComment.getId() == null) { |
|||
UUID uuid = Uuids.timeBased(); |
|||
alarmComment.setId(new AlarmCommentId(uuid)); |
|||
alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid)); |
|||
} |
|||
return alarmCommentDao.createAlarmComment(tenantId, alarmComment); |
|||
} |
|||
|
|||
private AlarmComment 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(); |
|||
((ObjectNode) comment).put("edited", "true"); |
|||
((ObjectNode) comment).put("editedOn", System.currentTimeMillis()); |
|||
existing.setComment(comment); |
|||
} |
|||
return alarmCommentDao.save(tenantId, existing); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
/** |
|||
* 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.alarm.AlarmCommentType; |
|||
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<T extends AlarmComment> extends BaseSqlEntity<T> implements BaseEntity<T> { |
|||
|
|||
@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 AlarmCommentType 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()); |
|||
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; |
|||
} |
|||
} |
|||
@ -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<AlarmComment> { |
|||
|
|||
public AlarmCommentEntity() { |
|||
super(); |
|||
} |
|||
|
|||
public AlarmCommentEntity(AlarmCommentInfo alarmCommentInfo) { |
|||
super(alarmCommentInfo); |
|||
} |
|||
|
|||
public AlarmCommentEntity(AlarmComment alarmComment) { |
|||
super(alarmComment); |
|||
} |
|||
|
|||
@Override |
|||
public AlarmComment toData() { |
|||
return super.toAlarmComment(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
public class AlarmCommentInfoEntity extends AbstractAlarmCommentEntity<AlarmCommentInfo> { |
|||
|
|||
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); |
|||
} |
|||
} |
|||
@ -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<AlarmComment> { |
|||
|
|||
@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!"); |
|||
} |
|||
} |
|||
} |
|||
@ -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<AlarmCommentEntity, UUID> { |
|||
|
|||
@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<AlarmCommentInfoEntity> findAllByAlarmId(@Param("alarmId") UUID alarmId, |
|||
Pageable pageable); |
|||
} |
|||
@ -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.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<AlarmCommentEntity, AlarmComment> implements AlarmCommentDao { |
|||
private final SqlPartitioningRepository partitioningRepository; |
|||
@Value("${sql.alarm_comments.partition_size:168}") |
|||
private int partitionSizeInHours; |
|||
|
|||
@Autowired |
|||
private AlarmCommentRepository alarmCommentRepository; |
|||
|
|||
@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<AlarmCommentInfo> 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<AlarmComment> findAlarmCommentByIdAsync(TenantId tenantId, UUID key) { |
|||
log.trace("Try to find alarm comment by id using [{}]", key); |
|||
return findByIdAsync(tenantId, key); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<AlarmCommentEntity> getEntityClass() { |
|||
return AlarmCommentEntity.class; |
|||
} |
|||
|
|||
@Override |
|||
protected JpaRepository<AlarmCommentEntity, UUID> getRepository() { |
|||
return alarmCommentRepository; |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
import static org.thingsboard.server.common.data.alarm.AlarmCommentType.OTHER; |
|||
|
|||
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); |
|||
|
|||
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<AlarmCommentInfo> 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); |
|||
|
|||
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); |
|||
|
|||
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<AlarmCommentInfo> 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); |
|||
|
|||
Assert.assertNotNull(createdComment); |
|||
Assert.assertNotNull(createdComment.getId()); |
|||
|
|||
alarmCommentService.deleteAlarmComment(tenantId, createdComment.getId()); |
|||
|
|||
AlarmComment fetched = alarmCommentService.findAlarmCommentByIdAsync(tenantId, createdComment.getId()).get(); |
|||
|
|||
Assert.assertNull("Alarm comment was returned when it was expected to be null", fetched); |
|||
} |
|||
} |
|||
@ -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 { |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
/** |
|||
* 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.AlarmCommentType; |
|||
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, AlarmCommentType.OTHER); |
|||
saveAlarmComment(commentId2, alarmId1, userId, AlarmCommentType.OTHER); |
|||
saveAlarmComment(commentId3, alarmId2, userId, AlarmCommentType.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, AlarmCommentType 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); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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.rule.engine.deduplication; |
|||
|
|||
public enum DeduplicationId { |
|||
|
|||
ORIGINATOR, TENANT, CUSTOMER |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* 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.rule.engine.deduplication; |
|||
|
|||
public enum DeduplicationStrategy { |
|||
|
|||
FIRST, LAST, ALL |
|||
|
|||
} |
|||
@ -0,0 +1,237 @@ |
|||
/** |
|||
* 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.rule.engine.deduplication; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ArrayNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.util.Pair; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.rule.engine.api.RuleNode; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNode; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.TbRelationTypes; |
|||
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentType; |
|||
import org.thingsboard.server.common.data.util.TbPair; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Comparator; |
|||
import java.util.HashMap; |
|||
import java.util.Iterator; |
|||
import java.util.LinkedList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@RuleNode( |
|||
type = ComponentType.ACTION, |
|||
name = "deduplication", |
|||
configClazz = TbMsgDeduplicationNodeConfiguration.class, |
|||
nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.", |
|||
nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages: <br></br>" + |
|||
"<b>FIRST</b> - return first message that arrived during deduplication period.<br></br>" + |
|||
"<b>LAST</b> - return last message that arrived during deduplication period.<br></br>" + |
|||
"<b>ALL</b> - return all messages as a single JSON array message. Where each element represents object with <b>msg</b> and <b>metadata</b> inner properties.<br></br>", |
|||
icon = "content_copy", |
|||
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
|||
configDirective = "tbActionNodeMsgDeduplicationConfig" |
|||
) |
|||
@Slf4j |
|||
public class TbMsgDeduplicationNode implements TbNode { |
|||
|
|||
private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; |
|||
private static final int TB_MSG_DEDUPLICATION_TIMEOUT = 5000; |
|||
public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10; |
|||
|
|||
private TbMsgDeduplicationNodeConfiguration config; |
|||
|
|||
private final Map<EntityId, List<TbMsg>> deduplicationMap; |
|||
private long deduplicationInterval; |
|||
private long lastScheduledTs; |
|||
private DeduplicationId deduplicationId; |
|||
|
|||
public TbMsgDeduplicationNode() { |
|||
this.deduplicationMap = new HashMap<>(); |
|||
} |
|||
|
|||
@Override |
|||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
|||
this.config = TbNodeUtils.convert(configuration, TbMsgDeduplicationNodeConfiguration.class); |
|||
this.deduplicationInterval = TimeUnit.SECONDS.toMillis(config.getInterval()); |
|||
this.deduplicationId = config.getId(); |
|||
scheduleTickMsg(ctx); |
|||
} |
|||
|
|||
@Override |
|||
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { |
|||
if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) { |
|||
try { |
|||
processDeduplication(ctx); |
|||
} finally { |
|||
scheduleTickMsg(ctx); |
|||
} |
|||
} else { |
|||
processOnRegularMsg(ctx, msg); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
deduplicationMap.clear(); |
|||
} |
|||
|
|||
private void processOnRegularMsg(TbContext ctx, TbMsg msg) { |
|||
EntityId id = getDeduplicationId(ctx, msg); |
|||
List<TbMsg> deduplicationMsgs = deduplicationMap.computeIfAbsent(id, k -> new LinkedList<>()); |
|||
if (deduplicationMsgs.size() < config.getMaxPendingMsgs()) { |
|||
log.trace("[{}][{}] Adding msg: [{}][{}] to the pending msgs map ...", ctx.getSelfId(), id, msg.getId(), msg.getMetaDataTs()); |
|||
deduplicationMsgs.add(msg); |
|||
ctx.ack(msg); |
|||
} else { |
|||
log.trace("[{}] Max limit of pending messages reached for deduplication id: [{}]", ctx.getSelfId(), id); |
|||
ctx.tellFailure(msg, new RuntimeException("[" + ctx.getSelfId() + "] Max limit of pending messages reached for deduplication id: [" + id + "]")); |
|||
} |
|||
} |
|||
|
|||
private EntityId getDeduplicationId(TbContext ctx, TbMsg msg) { |
|||
switch (deduplicationId) { |
|||
case ORIGINATOR: |
|||
return msg.getOriginator(); |
|||
case TENANT: |
|||
return ctx.getTenantId(); |
|||
case CUSTOMER: |
|||
return msg.getCustomerId(); |
|||
default: |
|||
throw new IllegalStateException("Unsupported deduplication id: " + deduplicationId); |
|||
} |
|||
} |
|||
|
|||
private void processDeduplication(TbContext ctx) { |
|||
if (deduplicationMap.isEmpty()) { |
|||
return; |
|||
} |
|||
List<TbMsg> deduplicationResults = new ArrayList<>(); |
|||
long deduplicationTimeoutMs = System.currentTimeMillis(); |
|||
deduplicationMap.forEach((entityId, tbMsgs) -> { |
|||
if (tbMsgs.isEmpty()) { |
|||
return; |
|||
} |
|||
Optional<TbPair<Long, Long>> packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs); |
|||
while (packBoundsOpt.isPresent()) { |
|||
TbPair<Long, Long> packBounds = packBoundsOpt.get(); |
|||
if (DeduplicationStrategy.ALL.equals(config.getStrategy())) { |
|||
List<TbMsg> pack = new ArrayList<>(); |
|||
for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) { |
|||
TbMsg msg = iterator.next(); |
|||
long msgTs = msg.getMetaDataTs(); |
|||
if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) { |
|||
pack.add(msg); |
|||
iterator.remove(); |
|||
} |
|||
} |
|||
deduplicationResults.add(TbMsg.newMsg( |
|||
config.getQueueName(), |
|||
config.getOutMsgType(), |
|||
entityId, |
|||
getMetadata(), |
|||
getMergedData(pack))); |
|||
} else { |
|||
TbMsg resultMsg = null; |
|||
boolean searchMin = DeduplicationStrategy.FIRST.equals(config.getStrategy()); |
|||
for (Iterator<TbMsg> iterator = tbMsgs.iterator(); iterator.hasNext(); ) { |
|||
TbMsg msg = iterator.next(); |
|||
long msgTs = msg.getMetaDataTs(); |
|||
if (msgTs >= packBounds.getFirst() && msgTs < packBounds.getSecond()) { |
|||
iterator.remove(); |
|||
if (resultMsg == null |
|||
|| (searchMin && msg.getMetaDataTs() < resultMsg.getMetaDataTs()) |
|||
|| (!searchMin && msg.getMetaDataTs() > resultMsg.getMetaDataTs())) { |
|||
resultMsg = msg; |
|||
} |
|||
} |
|||
} |
|||
deduplicationResults.add(resultMsg); |
|||
} |
|||
packBoundsOpt = findValidPack(tbMsgs, deduplicationTimeoutMs); |
|||
} |
|||
}); |
|||
deduplicationResults.forEach(outMsg -> enqueueForTellNextWithRetry(ctx, outMsg, 0)); |
|||
} |
|||
|
|||
private Optional<TbPair<Long, Long>> findValidPack(List<TbMsg> msgs, long deduplicationTimeoutMs) { |
|||
Optional<TbMsg> min = msgs.stream().min(Comparator.comparing(TbMsg::getMetaDataTs)); |
|||
return min.map(minTsMsg -> { |
|||
long packStartTs = minTsMsg.getMetaDataTs(); |
|||
long packEndTs = packStartTs + deduplicationInterval; |
|||
if (packEndTs <= deduplicationTimeoutMs) { |
|||
return new TbPair<>(packStartTs, packEndTs); |
|||
} |
|||
return null; |
|||
}); |
|||
} |
|||
|
|||
private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) { |
|||
if (config.getMaxRetries() > retryAttempt) { |
|||
ctx.enqueueForTellNext(msg, TbRelationTypes.SUCCESS, |
|||
() -> { |
|||
log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt); |
|||
}, |
|||
throwable -> { |
|||
log.trace("[{}][{}][{}] Failed to enqueue deduplication output message due to: ", ctx.getSelfId(), msg.getOriginator(), retryAttempt, throwable); |
|||
ctx.schedule(() -> { |
|||
enqueueForTellNextWithRetry(ctx, msg, retryAttempt + 1); |
|||
}, TB_MSG_DEDUPLICATION_RETRY_DELAY, TimeUnit.SECONDS); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private void scheduleTickMsg(TbContext ctx) { |
|||
long curTs = System.currentTimeMillis(); |
|||
if (lastScheduledTs == 0L) { |
|||
lastScheduledTs = curTs; |
|||
} |
|||
lastScheduledTs += TB_MSG_DEDUPLICATION_TIMEOUT; |
|||
long curDelay = Math.max(0L, (lastScheduledTs - curTs)); |
|||
TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); |
|||
ctx.tellSelf(tickMsg, curDelay); |
|||
} |
|||
|
|||
private String getMergedData(List<TbMsg> msgs) { |
|||
ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); |
|||
msgs.forEach(msg -> { |
|||
ObjectNode msgNode = JacksonUtil.newObjectNode(); |
|||
msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); |
|||
msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); |
|||
mergedData.add(msgNode); |
|||
}); |
|||
return JacksonUtil.toString(mergedData); |
|||
} |
|||
|
|||
private TbMsgMetaData getMetadata() { |
|||
TbMsgMetaData metaData = new TbMsgMetaData(); |
|||
metaData.putValue("ts", String.valueOf(System.currentTimeMillis())); |
|||
return metaData; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
/** |
|||
* 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.rule.engine.deduplication; |
|||
|
|||
import lombok.Data; |
|||
import org.thingsboard.rule.engine.api.NodeConfiguration; |
|||
|
|||
@Data |
|||
public class TbMsgDeduplicationNodeConfiguration implements NodeConfiguration<TbMsgDeduplicationNodeConfiguration> { |
|||
|
|||
private int interval; |
|||
private DeduplicationId id; |
|||
private DeduplicationStrategy strategy; |
|||
|
|||
// Advanced settings:
|
|||
private int maxPendingMsgs; |
|||
private int maxRetries; |
|||
|
|||
// only for DeduplicationStrategy.ALL:
|
|||
private String outMsgType; |
|||
private String queueName; |
|||
|
|||
@Override |
|||
public TbMsgDeduplicationNodeConfiguration defaultConfiguration() { |
|||
TbMsgDeduplicationNodeConfiguration configuration = new TbMsgDeduplicationNodeConfiguration(); |
|||
configuration.setInterval(60); |
|||
configuration.setId(DeduplicationId.ORIGINATOR); |
|||
configuration.setStrategy(DeduplicationStrategy.FIRST); |
|||
configuration.setMaxPendingMsgs(100); |
|||
configuration.setMaxRetries(3); |
|||
return configuration; |
|||
} |
|||
} |
|||
@ -0,0 +1,402 @@ |
|||
/** |
|||
* 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.rule.engine.action; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ArrayNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.jupiter.api.AfterEach; |
|||
import org.junit.jupiter.api.Assertions; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.mockito.ArgumentCaptor; |
|||
import org.mockito.ArgumentMatchers; |
|||
import org.mockito.stubbing.Answer; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.rule.engine.api.TbContext; |
|||
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
|||
import org.thingsboard.rule.engine.api.TbNodeException; |
|||
import org.thingsboard.rule.engine.api.TbRelationTypes; |
|||
import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; |
|||
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; |
|||
import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.RuleNodeId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.TbMsgMetaData; |
|||
import org.thingsboard.server.common.msg.session.SessionMsgType; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Random; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
import java.util.concurrent.atomic.AtomicLong; |
|||
import java.util.function.Consumer; |
|||
|
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.ArgumentMatchers.isNull; |
|||
import static org.mockito.ArgumentMatchers.nullable; |
|||
import static org.mockito.Mockito.doAnswer; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.spy; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@Slf4j |
|||
public class TbMsgDeduplicationNodeTest { |
|||
|
|||
private static final String MAIN_QUEUE_NAME = "Main"; |
|||
private static final String HIGH_PRIORITY_QUEUE_NAME = "HighPriority"; |
|||
private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; |
|||
|
|||
private TbContext ctx; |
|||
|
|||
private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); |
|||
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(factory); |
|||
private final int deduplicationInterval = 1; |
|||
|
|||
private TenantId tenantId; |
|||
|
|||
private TbMsgDeduplicationNode node; |
|||
private TbMsgDeduplicationNodeConfiguration config; |
|||
private TbNodeConfiguration nodeConfiguration; |
|||
|
|||
private CountDownLatch awaitTellSelfLatch; |
|||
|
|||
@BeforeEach |
|||
public void init() throws TbNodeException { |
|||
ctx = mock(TbContext.class); |
|||
|
|||
tenantId = TenantId.fromUUID(UUID.randomUUID()); |
|||
RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); |
|||
|
|||
when(ctx.getSelfId()).thenReturn(ruleNodeId); |
|||
when(ctx.getTenantId()).thenReturn(tenantId); |
|||
|
|||
doAnswer((Answer<TbMsg>) invocationOnMock -> { |
|||
String type = (String) (invocationOnMock.getArguments())[1]; |
|||
EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; |
|||
TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; |
|||
String data = (String) (invocationOnMock.getArguments())[4]; |
|||
return TbMsg.newMsg(type, originator, metaData.copy(), data); |
|||
}).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); |
|||
node = spy(new TbMsgDeduplicationNode()); |
|||
config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); |
|||
} |
|||
|
|||
private void invokeTellSelf(int maxNumberOfInvocation) { |
|||
invokeTellSelf(maxNumberOfInvocation, false, 0); |
|||
} |
|||
|
|||
private void invokeTellSelf(int maxNumberOfInvocation, boolean delayScheduleTimeout, int delayMultiplier) { |
|||
AtomicLong scheduleTimeout = new AtomicLong(deduplicationInterval); |
|||
AtomicInteger scheduleCount = new AtomicInteger(0); |
|||
doAnswer((Answer<Void>) invocationOnMock -> { |
|||
scheduleCount.getAndIncrement(); |
|||
if (scheduleCount.get() <= maxNumberOfInvocation) { |
|||
TbMsg msg = (TbMsg) (invocationOnMock.getArguments())[0]; |
|||
executorService.schedule(() -> { |
|||
try { |
|||
node.onMsg(ctx, msg); |
|||
awaitTellSelfLatch.countDown(); |
|||
} catch (ExecutionException | InterruptedException | TbNodeException e) { |
|||
log.error("Failed to execute tellSelf method call due to: ", e); |
|||
} |
|||
}, scheduleTimeout.get(), TimeUnit.SECONDS); |
|||
if (delayScheduleTimeout) { |
|||
scheduleTimeout.set(scheduleTimeout.get() * delayMultiplier); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
}).when(ctx).tellSelf(ArgumentMatchers.any(TbMsg.class), ArgumentMatchers.anyLong()); |
|||
} |
|||
|
|||
@AfterEach |
|||
public void destroy() { |
|||
executorService.shutdown(); |
|||
node.destroy(); |
|||
} |
|||
|
|||
@Test |
|||
public void given_100_messages_strategy_first_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { |
|||
int wantedNumberOfTellSelfInvocation = 2; |
|||
int msgCount = 100; |
|||
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); |
|||
invokeTellSelf(wantedNumberOfTellSelfInvocation); |
|||
|
|||
config.setInterval(deduplicationInterval); |
|||
config.setMaxPendingMsgs(msgCount); |
|||
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
node.init(ctx, nodeConfiguration); |
|||
|
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
long currentTimeMillis = System.currentTimeMillis(); |
|||
|
|||
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); |
|||
for (TbMsg msg : inputMsgs) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
|
|||
TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); |
|||
node.onMsg(ctx, msgToReject); |
|||
|
|||
awaitTellSelfLatch.await(); |
|||
|
|||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|||
|
|||
verify(ctx, times(msgCount)).ack(any()); |
|||
verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); |
|||
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|||
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|||
Assertions.assertEquals(inputMsgs.get(0), newMsgCaptor.getValue()); |
|||
} |
|||
|
|||
@Test |
|||
public void given_100_messages_strategy_last_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { |
|||
int wantedNumberOfTellSelfInvocation = 2; |
|||
int msgCount = 100; |
|||
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); |
|||
invokeTellSelf(wantedNumberOfTellSelfInvocation); |
|||
|
|||
config.setStrategy(DeduplicationStrategy.LAST); |
|||
config.setInterval(deduplicationInterval); |
|||
config.setMaxPendingMsgs(msgCount); |
|||
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
node.init(ctx, nodeConfiguration); |
|||
|
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
long currentTimeMillis = System.currentTimeMillis(); |
|||
|
|||
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); |
|||
TbMsg msgWithLatestTs = getMsgWithLatestTs(inputMsgs); |
|||
|
|||
for (TbMsg msg : inputMsgs) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
|
|||
TbMsg msgToReject = createMsg(deviceId, inputMsgs.get(inputMsgs.size() - 1).getMetaDataTs() + 2); |
|||
node.onMsg(ctx, msgToReject); |
|||
|
|||
awaitTellSelfLatch.await(); |
|||
|
|||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|||
|
|||
verify(ctx, times(msgCount)).ack(any()); |
|||
verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); |
|||
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); |
|||
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|||
Assertions.assertEquals(msgWithLatestTs, newMsgCaptor.getValue()); |
|||
} |
|||
|
|||
@Test |
|||
public void given_100_messages_strategy_all_then_verifyOutput() throws TbNodeException, ExecutionException, InterruptedException { |
|||
int wantedNumberOfTellSelfInvocation = 2; |
|||
int msgCount = 100; |
|||
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); |
|||
invokeTellSelf(wantedNumberOfTellSelfInvocation); |
|||
|
|||
config.setInterval(deduplicationInterval); |
|||
config.setStrategy(DeduplicationStrategy.ALL); |
|||
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); |
|||
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); |
|||
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
node.init(ctx, nodeConfiguration); |
|||
|
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
long currentTimeMillis = System.currentTimeMillis(); |
|||
|
|||
List<TbMsg> inputMsgs = getTbMsgs(deviceId, msgCount, currentTimeMillis, 500); |
|||
for (TbMsg msg : inputMsgs) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
|
|||
awaitTellSelfLatch.await(); |
|||
|
|||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|||
|
|||
verify(ctx, times(msgCount)).ack(any()); |
|||
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|||
verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|||
|
|||
Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); |
|||
TbMsg outMessage = newMsgCaptor.getAllValues().get(0); |
|||
Assertions.assertEquals(getMergedData(inputMsgs), outMessage.getData()); |
|||
Assertions.assertEquals(deviceId, outMessage.getOriginator()); |
|||
Assertions.assertEquals(config.getOutMsgType(), outMessage.getType()); |
|||
Assertions.assertEquals(config.getQueueName(), outMessage.getQueueName()); |
|||
} |
|||
|
|||
@Test |
|||
public void given_100_messages_strategy_all_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { |
|||
int wantedNumberOfTellSelfInvocation = 2; |
|||
int msgCount = 100; |
|||
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); |
|||
invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); |
|||
|
|||
config.setInterval(deduplicationInterval); |
|||
config.setStrategy(DeduplicationStrategy.ALL); |
|||
config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); |
|||
config.setQueueName(HIGH_PRIORITY_QUEUE_NAME); |
|||
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
node.init(ctx, nodeConfiguration); |
|||
|
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
long currentTimeMillis = System.currentTimeMillis(); |
|||
|
|||
List<TbMsg> firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); |
|||
for (TbMsg msg : firstMsgPack) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); |
|||
|
|||
List<TbMsg> secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); |
|||
for (TbMsg msg : secondMsgPack) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
|
|||
awaitTellSelfLatch.await(); |
|||
|
|||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|||
|
|||
verify(ctx, times(msgCount)).ack(any()); |
|||
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|||
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|||
|
|||
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues(); |
|||
Assertions.assertEquals(2, resultMsgs.size()); |
|||
|
|||
TbMsg firstMsg = resultMsgs.get(0); |
|||
Assertions.assertEquals(getMergedData(firstMsgPack), firstMsg.getData()); |
|||
Assertions.assertEquals(deviceId, firstMsg.getOriginator()); |
|||
Assertions.assertEquals(config.getOutMsgType(), firstMsg.getType()); |
|||
Assertions.assertEquals(config.getQueueName(), firstMsg.getQueueName()); |
|||
|
|||
TbMsg secondMsg = resultMsgs.get(1); |
|||
Assertions.assertEquals(getMergedData(secondMsgPack), secondMsg.getData()); |
|||
Assertions.assertEquals(deviceId, secondMsg.getOriginator()); |
|||
Assertions.assertEquals(config.getOutMsgType(), secondMsg.getType()); |
|||
Assertions.assertEquals(config.getQueueName(), secondMsg.getQueueName()); |
|||
} |
|||
|
|||
@Test |
|||
public void given_100_messages_strategy_last_then_verifyOutput_2_packs() throws TbNodeException, ExecutionException, InterruptedException { |
|||
int wantedNumberOfTellSelfInvocation = 2; |
|||
int msgCount = 100; |
|||
awaitTellSelfLatch = new CountDownLatch(wantedNumberOfTellSelfInvocation); |
|||
invokeTellSelf(wantedNumberOfTellSelfInvocation, true, 3); |
|||
|
|||
config.setInterval(deduplicationInterval); |
|||
config.setStrategy(DeduplicationStrategy.LAST); |
|||
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
|||
node.init(ctx, nodeConfiguration); |
|||
|
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
long currentTimeMillis = System.currentTimeMillis(); |
|||
|
|||
List<TbMsg> firstMsgPack = getTbMsgs(deviceId, msgCount / 2, currentTimeMillis, 500); |
|||
for (TbMsg msg : firstMsgPack) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
long firstPackDeduplicationPackEndTs = firstMsgPack.get(0).getMetaDataTs() + TimeUnit.SECONDS.toMillis(deduplicationInterval); |
|||
TbMsg msgWithLatestTsInFirstPack = getMsgWithLatestTs(firstMsgPack); |
|||
|
|||
List<TbMsg> secondMsgPack = getTbMsgs(deviceId, msgCount / 2, firstPackDeduplicationPackEndTs, 500); |
|||
for (TbMsg msg : secondMsgPack) { |
|||
node.onMsg(ctx, msg); |
|||
} |
|||
TbMsg msgWithLatestTsInSecondPack = getMsgWithLatestTs(secondMsgPack); |
|||
|
|||
awaitTellSelfLatch.await(); |
|||
|
|||
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); |
|||
ArgumentCaptor<Runnable> successCaptor = ArgumentCaptor.forClass(Runnable.class); |
|||
ArgumentCaptor<Consumer<Throwable>> failureCaptor = ArgumentCaptor.forClass(Consumer.class); |
|||
|
|||
verify(ctx, times(msgCount)).ack(any()); |
|||
verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); |
|||
verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); |
|||
|
|||
List<TbMsg> resultMsgs = newMsgCaptor.getAllValues(); |
|||
Assertions.assertEquals(2, resultMsgs.size()); |
|||
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInFirstPack)); |
|||
Assertions.assertTrue(resultMsgs.contains(msgWithLatestTsInSecondPack)); |
|||
} |
|||
|
|||
private TbMsg getMsgWithLatestTs(List<TbMsg> firstMsgPack) { |
|||
int indexOfLastMsgInArray = firstMsgPack.size() - 1; |
|||
int indexToSetMaxTs = new Random().nextInt(indexOfLastMsgInArray) + 1; |
|||
TbMsg currentMaxTsMsg = firstMsgPack.get(indexOfLastMsgInArray); |
|||
TbMsg newLastMsgOfArray = firstMsgPack.get(indexToSetMaxTs); |
|||
firstMsgPack.set(indexOfLastMsgInArray, newLastMsgOfArray); |
|||
firstMsgPack.set(indexToSetMaxTs, currentMaxTsMsg); |
|||
return currentMaxTsMsg; |
|||
} |
|||
|
|||
private List<TbMsg> getTbMsgs(DeviceId deviceId, int msgCount, long currentTimeMillis, int initTsStep) { |
|||
List<TbMsg> inputMsgs = new ArrayList<>(); |
|||
var ts = currentTimeMillis + initTsStep; |
|||
for (int i = 0; i < msgCount; i++) { |
|||
inputMsgs.add(createMsg(deviceId, ts)); |
|||
ts += 2; |
|||
} |
|||
return inputMsgs; |
|||
} |
|||
|
|||
private TbMsg createMsg(DeviceId deviceId, long ts) { |
|||
ObjectNode dataNode = JacksonUtil.newObjectNode(); |
|||
dataNode.put("deviceId", deviceId.getId().toString()); |
|||
TbMsgMetaData metaData = new TbMsgMetaData(); |
|||
metaData.putValue("ts", String.valueOf(ts)); |
|||
return TbMsg.newMsg( |
|||
MAIN_QUEUE_NAME, |
|||
SessionMsgType.POST_TELEMETRY_REQUEST.name(), |
|||
deviceId, |
|||
metaData, |
|||
JacksonUtil.toString(dataNode)); |
|||
} |
|||
|
|||
private String getMergedData(List<TbMsg> msgs) { |
|||
ArrayNode mergedData = JacksonUtil.OBJECT_MAPPER.createArrayNode(); |
|||
msgs.forEach(msg -> { |
|||
ObjectNode msgNode = JacksonUtil.newObjectNode(); |
|||
msgNode.set("msg", JacksonUtil.toJsonNode(msg.getData())); |
|||
msgNode.set("metadata", JacksonUtil.valueToTree(msg.getMetaData().getData())); |
|||
mergedData.add(msgNode); |
|||
}); |
|||
return JacksonUtil.toString(mergedData); |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue