diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index e5ef53d189..196a660e00 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -136,7 +136,9 @@ public class AlarmController extends BaseController { @ResponseBody public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { alarm.setTenantId(getTenantId()); + checkNotNull(alarm.getOriginator()); checkEntity(alarm.getId(), alarm, Resource.ALARM); + checkEntityId(alarm.getOriginator(), Operation.READ); return tbAlarmService.save(alarm, getCurrentUser()); } @@ -159,11 +161,12 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.ack(alarm, getCurrentUser()).get(); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.ack(alarm, getCurrentUser()); } @ApiOperation(value = "Clear Alarm (clearAlarm)", @@ -173,11 +176,12 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.clear(alarm, getCurrentUser()).get(); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.clear(alarm, getCurrentUser()); } @ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)", diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index 156f4ba772..f82f97ebee 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -72,7 +72,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { break; case ALARM_ACK_RPC_MESSAGE: if (existentAlarm != null) { - alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); + alarmService.acknowledgeAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); } break; case ALARM_CLEAR_RPC_MESSAGE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 50436bea27..cd2c6d4101 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -24,14 +24,20 @@ import org.thingsboard.common.util.JacksonUtil; 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.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -47,7 +53,16 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb TenantId tenantId = alarm.getTenantId(); try { Alarm savedAlarm = checkNotNull(alarmSubscriptionService.createOrUpdateAlarm(alarm)); - notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); + AlarmApiCallResult result; + if (alarm.getId() == null) { + result = alarmSubscriptionService.createAlarm(CreateOrUpdateActiveAlarmRequest.fromAlarm(alarm)); + } else { + result = alarmSubscriptionService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm)); + } + actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED; + if (result.isModified()) { + notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); + } return savedAlarm; } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); @@ -56,59 +71,100 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture ack(Alarm alarm, User user) { - long ackTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); - return Futures.transform(future, result -> { + public AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), System.currentTimeMillis()); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isModified()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ACK")) .build(); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); - alarm.setAckTs(ackTs); - alarm.setAcknowledged(true); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user); + } else { + throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); } @Override - public ListenableFuture clear(Alarm alarm, User user) { - long clearTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); - return Futures.transform(future, result -> { + public AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), System.currentTimeMillis(), null); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isCleared()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "CLEAR")) .build(); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); - alarm.setClearTs(clearTs); - alarm.setCleared(true); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user); + } else { + throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); } @Override - public Alarm assign(Alarm alarm, User user, UserId assigneeId) { - long assignTs = System.currentTimeMillis(); - AlarmOperationResult operationResult = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, assignTs); - notificationEntityService.notifyCreateOrUpdateAlarm(operationResult.getAlarm(), ActionType.ALARM_ASSIGN, user); - return operationResult.getAlarm(); + public AlarmInfo assign(Alarm alarm, User user, UserId assigneeId) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, System.currentTimeMillis()); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmAssignee assignee = alarmInfo.getAssignee(); + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), + (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) + .put("userId", user.getId().toString()) + .put("assigneeId", assignee.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; } @Override - public Alarm unassign(Alarm alarm, User user) { - long assignTs = System.currentTimeMillis(); - AlarmOperationResult operationResult = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs); - notificationEntityService.notifyCreateOrUpdateAlarm(operationResult.getAlarm(), ActionType.ALARM_UNASSIGN, user); - return operationResult.getAlarm(); + public AlarmInfo unassign(Alarm alarm, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), System.currentTimeMillis()); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index 1501938df1..e4b40f1305 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.UserId; @@ -25,13 +25,13 @@ public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; - ListenableFuture ack(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException; - ListenableFuture clear(Alarm alarm, User user); + AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException; - Alarm assign(Alarm alarm, User user, UserId assigneeId); + AlarmInfo assign(Alarm alarm, User user, UserId assigneeId) throws ThingsboardException; - Alarm unassign(Alarm alarm, User user); + AlarmInfo unassign(Alarm alarm, User user) throws ThingsboardException; Boolean delete(Alarm alarm, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 71d58b48a3..e9c0f5247c 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -33,6 +33,8 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -45,6 +47,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; @@ -93,6 +96,37 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return "alarm"; } + @Override + public AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request) { + boolean creationEnabled = apiUsageStateService.getApiUsageState(request.getTenantId()).isAlarmCreationEnabled(); + return withWsCallback(alarmService.createAlarm(request, creationEnabled)); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + return withWsCallback(alarmService.updateAlarm(request)); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withWsCallback(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { + return withWsCallback(alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { + return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs)); + } + @Override public Alarm createOrUpdateAlarm(Alarm alarm) { AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); @@ -123,44 +157,22 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { - ListenableFuture result = alarmService.ackAlarm(tenantId, alarmId, ackTs); + ListenableFuture result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = clearAlarmForResult(tenantId, alarmId, details, clearTs); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); - Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return result; - } - - @Override - public AlarmOperationResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { - AlarmOperationResult result = alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs); - if (result.isSuccessful()) { - onAlarmUpdated(result); - } else { - log.warn("[{}][{}] Failed to assign alarm.", tenantId, alarmId); - } - return result; - } - - @Override - public AlarmOperationResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { - AlarmOperationResult result = alarmService.unassignAlarm(tenantId, alarmId, assignTs); - if (result.isSuccessful()) { - onAlarmUpdated(result); - } else { - log.warn("[{}][{}] Failed to unassign alarm.", tenantId, alarmId); - } - return result; + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.immediateFuture(new AlarmOperationResult(result)); } @Override @@ -203,7 +215,28 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); } + @Deprecated private void onAlarmUpdated(AlarmOperationResult result) { + wsCallBackExecutor.submit(() -> { + Alarm alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, alarm, null, TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, null, alarm); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + }); + } + + private void onAlarmUpdated(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { Alarm alarm = result.getAlarm(); TenantId tenantId = alarm.getTenantId(); @@ -243,9 +276,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } - private class AlarmUpdateCallback implements FutureCallback { + private class AlarmUpdateCallback implements FutureCallback { @Override - public void onSuccess(@Nullable AlarmOperationResult result) { + public void onSuccess(@Nullable AlarmApiCallResult result) { onAlarmUpdated(result); } @@ -255,4 +288,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } } + private AlarmApiCallResult withWsCallback(AlarmApiCallResult result) { + if (result.isSuccessful() && result.isModified()) { + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + } + return result; + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index b7755ef4d9..c81950c8d1 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -29,8 +29,10 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; @@ -83,15 +85,18 @@ public class DefaultTbAlarmServiceTest { @Test public void testSave() throws ThingsboardException { var alarm = new AlarmInfo(); - when(alarmSubscriptionService.createOrUpdateAlarm(alarm)).thenReturn(alarm); + when(alarmSubscriptionService.createAlarm(any())).thenReturn(AlarmApiCallResult.builder() + .successful(true) + .alarm(alarm) + .build()); service.save(alarm, new User()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).createOrUpdateAlarm(eq(alarm)); + verify(alarmSubscriptionService, times(1)).createAlarm(any()); } @Test - public void testAck() { + public void testAck() throws ThingsboardException { var alarm = new Alarm(); when(alarmSubscriptionService.ackAlarm(any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); service.ack(alarm, new User(new UserId(UUID.randomUUID()))); @@ -102,7 +107,7 @@ public class DefaultTbAlarmServiceTest { } @Test - public void testClear() { + public void testClear() throws ThingsboardException { var alarm = new Alarm(); alarm.setAcknowledged(true); when(alarmSubscriptionService.clearAlarm(any(), any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java new file mode 100644 index 0000000000..075216565b --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + + +@Data +public class AlarmApiCallResult { + + private final boolean successful; + private final boolean created; + private final boolean modified; + private final boolean cleared; + private final AlarmInfo alarm; + private final Alarm old; + private final List propagatedEntitiesList; + + @Builder + private AlarmApiCallResult(boolean successful, boolean created, boolean modified, boolean cleared, AlarmInfo alarm, Alarm old, List propagatedEntitiesList) { + this.successful = successful; + this.created = created; + this.modified = modified; + this.cleared = cleared; + this.alarm = alarm; + this.old = old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + public AlarmApiCallResult(AlarmApiCallResult other, List propagatedEntitiesList) { + this.successful = other.successful; + this.created = other.created; + this.modified = other.modified; + this.cleared = other.cleared; + this.alarm = other.alarm; + this.old = other.old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + boolean hasSeverityChange() { + if (alarm == null || old == null) { + return false; + } else { + return !alarm.getSeverity().equals(old.getSeverity()); + } + } + + public AlarmSeverity getOldSeverity() { + return hasSeverityChange() ? old.getSeverity() : null; + } + + public boolean isPropagationChanged() { + if (alarm == null || old == null) { + return false; + } + return (alarm.isPropagate() != old.isPropagate()) || + (alarm.isPropagateToOwner() != old.isPropagateToOwner()) || + (alarm.isPropagateToTenant() != old.isPropagateToTenant()) || + (!alarm.getPropagateRelationTypes().equals(old.getPropagateRelationTypes())); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java index 8f829b6d88..e3a05a4e2f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.alarm; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; @@ -25,8 +26,10 @@ import org.thingsboard.server.common.data.id.EntityId; import java.util.Collections; import java.util.List; +@Builder @Data @AllArgsConstructor +@Deprecated public class AlarmOperationResult { private final Alarm alarm; private final boolean successful; @@ -34,18 +37,12 @@ public class AlarmOperationResult { private final AlarmSeverity oldSeverity; private final List propagatedEntitiesList; - private final AlarmAssigneeUpdate assigneeUpdate; - - public AlarmOperationResult(Alarm alarm, AlarmAssigneeUpdate assigneeUpdate, List propagatedEntitiesList) { - this(alarm, true, false, null, propagatedEntitiesList, assigneeUpdate); - } - public AlarmOperationResult(Alarm alarm, boolean successful) { this(alarm, successful, Collections.emptyList()); } public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { - this(alarm, successful, false, null, propagatedEntitiesList, null); + this(alarm, successful, false, null, propagatedEntitiesList); } public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List propagatedEntitiesList) { @@ -54,6 +51,14 @@ public class AlarmOperationResult { this.created = created; this.propagatedEntitiesList = propagatedEntitiesList; this.oldSeverity = null; - this.assigneeUpdate = null; + } + + //Temporary while we have not removed the AlarmOperationResult. + public AlarmOperationResult(AlarmApiCallResult result) { + this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null; + this.successful = result.isSuccessful() && (result.isCreated() || result.isModified()); + this.created = result.isCreated(); + this.oldSeverity = result.getOldSeverity(); + this.propagatedEntitiesList = result.getPropagatedEntitiesList(); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index e160053ff9..aa973a18ae 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -40,43 +40,56 @@ import java.util.Collection; public interface AlarmService extends EntityDaoService { - // New API, since 3.5. + /* + * New API, since 3.5. + */ + /** * Designed for atomic operations over active alarms. * Only one active alarm may exist for the pair {originatorId, alarmType} */ - AlarmOperationResult createAlarm(CreateOrUpdateActiveAlarmRequest request); + AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request); /** * Designed for atomic operations over active alarms. * Only one active alarm may exist for the pair {originatorId, alarmType} */ - AlarmOperationResult createAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled); + AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled); /** * Designed to update existing alarm. Accepts only part of the alarm fields. - * */ - AlarmOperationResult updateAlarm(AlarmUpdateRequest request); + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); - // Legacy API, before 3.5 + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); - AlarmOperationResult createOrUpdateAlarm(Alarm alarm); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); - AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); - // Other API + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long ts); - AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); + AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId); + + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5.0", forRemoval = true) + AlarmOperationResult createOrUpdateAlarm(Alarm alarm); + + @Deprecated(since = "3.5.0", forRemoval = true) + AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); - AlarmOperationResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); - - AlarmOperationResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long ts); + @Deprecated(since = "3.5.0", forRemoval = true) + AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); + // Other API Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java index abe8626ad7..20ed7b549c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.common.data.alarm; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.UserId; import java.io.Serializable; +@Builder +@AllArgsConstructor @Data public class AlarmAssignee implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java new file mode 100644 index 0000000000..27c31854ef --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 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 org.thingsboard.server.common.data.id.TenantId; + +public interface AlarmModificationRequest { + + TenantId getTenantId(); + + long getStartTs(); + + long getEndTs(); + + void setStartTs(long startTs); + + void setEndTs(long endTs); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java index 78d62de523..10069f8dbe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java @@ -16,19 +16,28 @@ package org.thingsboard.server.common.data.alarm; import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Collections; import java.util.List; +@Builder @Data public class AlarmPropagationInfo { + public static AlarmPropagationInfo EMPTY = new AlarmPropagationInfo(false, false, false, Collections.emptyList()); + @ApiModelProperty(position = 1, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; @ApiModelProperty(position = 2, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") private boolean propagateToOwner; @ApiModelProperty(position = 3, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") private boolean propagateToTenant; + @NoXss @ApiModelProperty(position = 4, value = "JSON array of relation types that should be used for propagation. " + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java index a5d88edb41..356097842b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -17,29 +17,54 @@ package org.thingsboard.server.common.data.alarm; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; @Data -public class AlarmUpdateRequest { +@Builder +public class AlarmUpdateRequest implements AlarmModificationRequest { + @NotNull @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private TenantId tenantId; + @NotNull @ApiModelProperty(position = 2, value = "JSON object with the alarm Id. " + "Specify this field to update the alarm. " + "Referencing non-existing alarm Id will cause error. " + "Omit this field to create new alarm.") private AlarmId alarmId; + @NotNull @ApiModelProperty(position = 3, required = true, value = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @ApiModelProperty(position = 4, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; @ApiModelProperty(position = 5, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; + @NoXss @ApiModelProperty(position = 6, value = "JSON object with alarm details") private JsonNode details; + @Valid @ApiModelProperty(position = 7, value = "JSON object with propagation details") private AlarmPropagationInfo propagation; + public static AlarmUpdateRequest fromAlarm(Alarm a) { + return AlarmUpdateRequest.builder() + .tenantId(a.getTenantId()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .build(); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/CreateOrUpdateActiveAlarmRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/CreateOrUpdateActiveAlarmRequest.java index afd4a2e537..e882b377f1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/CreateOrUpdateActiveAlarmRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/CreateOrUpdateActiveAlarmRequest.java @@ -17,34 +17,64 @@ package org.thingsboard.server.common.data.alarm; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; @Data -public class CreateOrUpdateActiveAlarmRequest { +@Builder +public class CreateOrUpdateActiveAlarmRequest implements AlarmModificationRequest { + @NotNull @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private TenantId tenantId; @ApiModelProperty(position = 2, value = "JSON object with Customer Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private CustomerId customerId; + @NotNull @ApiModelProperty(position = 3, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm") @Length(fieldName = "type") private String type; + @NotNull @ApiModelProperty(position = 4, required = true, value = "JSON object with alarm originator id") private EntityId originator; + @NotNull @ApiModelProperty(position = 5, required = true, value = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; @ApiModelProperty(position = 6, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; @ApiModelProperty(position = 7, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; + @NoXss @ApiModelProperty(position = 8, value = "JSON object with alarm details") private JsonNode details; + @Valid @ApiModelProperty(position = 9, value = "JSON object with propagation details") private AlarmPropagationInfo propagation; + public static CreateOrUpdateActiveAlarmRequest fromAlarm(Alarm a) { + return CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(a.getTenantId()) + .customerId(a.getCustomerId()) + .type(a.getType()) + .originator(a.getOriginator()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .build(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index c0960347bc..d3eaddf737 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -25,6 +25,14 @@ import java.util.UUID; */ public class EntityIdFactory { + public static EntityId getByTypeAndUuid(int type, String uuid) { + return getByTypeAndUuid(EntityType.values()[type], UUID.fromString(uuid)); + } + + public static EntityId getByTypeAndUuid(String type, String uuid) { + return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); + } + public static EntityId getByTypeAndId(String type, String uuid) { return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index acce2d04bf..e8c169897c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.dao.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; @@ -72,5 +74,16 @@ public interface AlarmDao extends Dao { void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); - AlarmInfo acknowledgeAlarm(TenantId tenantId, AlarmId id); + AlarmApiCallResult createOrUpdateActiveAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled); + + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 4d2348472a..3245a81ec5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -20,17 +20,16 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; 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.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -42,7 +41,6 @@ import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -57,8 +55,10 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.tenant.TenantService; import javax.annotation.Nullable; import java.util.ArrayList; @@ -77,34 +77,52 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AlarmDaoService") @Slf4j +@RequiredArgsConstructor public class BaseAlarmService extends AbstractEntityService implements AlarmService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; - @Autowired - private AlarmDao alarmDao; + private final TenantService tenantService; + private final AlarmDao alarmDao; + private final EntityService entityService; + private final DataValidator alarmDataValidator; - @Autowired - private EntityService entityService; - - @Autowired - private DataValidator alarmDataValidator; + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + validateAlarmRequest(request); + return withPropagated(alarmDao.updateAlarm(request)); + } + @Override + public AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request) { + return createAlarm(request, true); + } @Override - public AlarmOperationResult updateAlarm(AlarmUpdateRequest request) { - return null; + public AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled) { + validateAlarmRequest(request); + CustomerId customerId = entityService.fetchEntityCustomerId(request.getTenantId(), request.getOriginator()).orElse(null); + if (customerId == null && request.getCustomerId() != null) { + throw new DataValidationException("Can't assign alarm to customer. Originator is not assigned to customer!"); + } else if (customerId != null && request.getCustomerId() != null && !customerId.equals(request.getCustomerId())) { + throw new DataValidationException("Can't assign alarm to customer. Originator belongs to different customer!"); + } + request.setCustomerId(customerId); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, alarmCreationEnabled); + if (!result.isSuccessful() && !alarmCreationEnabled) { + throw new ApiUsageLimitsExceededException("Alarms creation is disabled"); + } + return withPropagated(result); } @Override - public AlarmOperationResult createAlarm(CreateOrUpdateActiveAlarmRequest request) { - return null; + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs)); } @Override - public AlarmOperationResult createAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled) { - return null; + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); } @Override @@ -154,6 +172,20 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + @Transactional + public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId) { + log.debug("Deleting Alarm Id: {}", alarmId); + AlarmInfo alarm = alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); + if (alarm == null) { + return AlarmApiCallResult.builder().successful(false).build(); + } else { + deleteEntityRelations(tenantId, alarm.getId()); + alarmDao.removeById(tenantId, alarm.getUuidId()); + return AlarmApiCallResult.builder().alarm(alarm).successful(true).build(); + } + } + @Override @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { @@ -175,7 +207,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } - private List createEntityAlarmRecords(Alarm alarm) throws InterruptedException, ExecutionException { + private List createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException { Set propagatedEntitiesSet = new LinkedHashSet<>(); propagatedEntitiesSet.add(alarm.getOriginator()); if (alarm.isPropagate()) { @@ -228,7 +260,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } else { propagatedEntitiesList = new ArrayList<>(getPropagationEntityIds(result)); } - return new AlarmOperationResult(result, true, false, oldAlarmSeverity, propagatedEntitiesList, null); + return new AlarmOperationResult(result, true, false, oldAlarmSeverity, propagatedEntitiesList); } @Override @@ -261,43 +293,13 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public AlarmOperationResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { - return getAndUpdate(tenantId, alarmId, new Function<>() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || assigneeId.equals(alarm.getAssigneeId())) { - return new AlarmOperationResult(alarm, false); - } else { - alarm.setAssigneeId(assigneeId); - alarm.setAssignTs(assignTime); - alarm = alarmDao.save(alarm.getTenantId(), alarm); - AlarmInfo alarmInfo = alarmDao.findAlarmInfoById(tenantId, alarm.getUuidId()); - return new AlarmOperationResult(alarm, - new AlarmAssigneeUpdate(false, alarmInfo.getAssignee()), - new ArrayList<>(getPropagationEntityIds(alarm))); - } - } - }); + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { + return withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime)); } @Override - public AlarmOperationResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTime) { - return getAndUpdate(tenantId, alarmId, new Function<>() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || alarm.getAssigneeId() == null) { - return new AlarmOperationResult(alarm, false); - } else { - alarm.setAssigneeId(null); - alarm.setAssignTs(assignTime); - alarm = alarmDao.save(alarm.getTenantId(), alarm); - return new AlarmOperationResult(alarm, new AlarmAssigneeUpdate(true, null), - new ArrayList<>(getPropagationEntityIds(alarm))); - } - } - }); + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime) { + return withPropagated(alarmDao.unassignAlarm(tenantId, alarmId, unassignTime)); } @Override @@ -340,7 +342,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } else if (alarmStatus != null) { asf = AlarmStatusFilter.from(alarmStatus); } else { - asf= AlarmStatusFilter.empty(); + asf = AlarmStatusFilter.empty(); } Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, asf, assigneeId); @@ -391,6 +393,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return existing; } + private List getPropagationEntityIdsList(Alarm alarm) { + return new ArrayList<>(getPropagationEntityIds(alarm)); + } + private Set getPropagationEntityIds(Alarm alarm) { if (alarm.isPropagate() || alarm.isPropagateToOwner() || alarm.isPropagateToTenant()) { List entityAlarms = alarmDao.findEntityAlarmRecords(alarm.getTenantId(), alarm.getId()); @@ -425,4 +431,39 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return EntityType.ALARM; } + //TODO: refactor to use efficient caching. + private AlarmApiCallResult withPropagated(AlarmApiCallResult result) { + if (result.isSuccessful() && result.getAlarm() != null) { + List propagationEntities; + if (result.isPropagationChanged()) { + try { + propagationEntities = createEntityAlarmRecords(result.getAlarm()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } else { + propagationEntities = getPropagationEntityIdsList(result.getAlarm()); + } + return new AlarmApiCallResult(result, propagationEntities); + } else { + return result; + } + } + + private void validateAlarmRequest(AlarmModificationRequest request) { + ConstraintValidator.validateFields(request); + if (request.getStartTs() > request.getEndTs()) { + throw new DataValidationException("Alarm start ts can't be greater then alarm end ts!"); + } + if (!tenantService.tenantExists(request.getTenantId())) { + throw new DataValidationException("Alarm is referencing to non-existent tenant!"); + } + if (request.getStartTs() == 0L) { + request.setStartTs(System.currentTimeMillis()); + } + if (request.getEndTs() == 0L) { + request.setEndTs(request.getStartTs()); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 4f6842e7a9..9a265cb5f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -291,7 +291,7 @@ public class ModelConstants { public static final String ALARM_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String ALARM_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ALARM_TYPE_PROPERTY = "type"; - public static final String ALARM_DETAILS_PROPERTY = "details"; + public static final String ALARM_DETAILS_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String ALARM_STATUS_PROPERTY = "status"; public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; @@ -314,6 +314,8 @@ public class ModelConstants { public static final String ALARM_PROPAGATE_TO_TENANT_PROPERTY = "propagate_to_tenant"; public static final String ALARM_PROPAGATE_RELATION_TYPES = "propagate_relation_types"; + public static final String ALARM_OPERATION_RESULT_PROPERTY = "operation_result"; + public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; public static final String ALARM_COMMENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index a183f199b1..164e1518a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -113,7 +113,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity private Long assignTs; @Type(type = "json") - @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) + @Column(name = ModelConstants.ALARM_DETAILS_PROPERTY) private JsonNode details; @Column(name = ALARM_PROPAGATE_PROPERTY) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java index 8dc86d14e4..40ecd19a02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java @@ -47,13 +47,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_VIEW_NAME; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ALARM_VIEW_NAME) -@NamedNativeQueries({ - @NamedNativeQuery( - name = "AlarmEntity.acknowledgeAlarm", - query = "SELECT * FROM acknowledge_alarm(:t_id, :a_id, :a_ts)", - resultClass = AlarmInfoEntity.class - ) -}) public class AlarmInfoEntity extends AbstractAlarmEntity { @Column(name = ALARM_ORIGINATOR_NAME_PROPERTY) @@ -73,16 +66,6 @@ public class AlarmInfoEntity extends AbstractAlarmEntity { super(); } - public AlarmInfoEntity(AlarmEntity alarmEntity, - String assigneeFirstName, - String assigneeLastName, - String assigneeEmail) { - super(alarmEntity); - this.assigneeFirstName = assigneeFirstName; - this.assigneeLastName = assigneeLastName; - this.assigneeEmail = assigneeEmail; - } - @Override public AlarmInfo toData() { AlarmInfo alarmInfo = new AlarmInfo(super.toAlarm()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index ff7370671f..04dc13c53d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -18,11 +18,11 @@ 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.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -179,8 +179,31 @@ public interface AlarmRepository extends JpaRepository { @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); - // See named native query for definition - AlarmInfoEntity acknowledgeAlarm(@Param("t_id") UUID tenantId, - @Param("a_id") UUID alarmId, - @Param("a_ts") long ts); + @Procedure(procedureName = "create_or_update_active_alarm") + String createOrUpdateActiveAlarm(@Param("t_id") UUID tenantId, @Param("c_id") UUID customerId, + @Param("a_id") UUID alarmId, @Param("a_created_ts") long createdTime, + @Param("a_o_id") UUID originatorId, @Param("a_o_type") int originatorType, + @Param("a_type") String type, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes, + @Param("a_creation_enabled") boolean alarmCreationEnabled); + + @Procedure(procedureName = "update_alarm") + String updateAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes); + + @Procedure(procedureName = "acknowledge_alarm") + String acknowledgeAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts); + + @Procedure(procedureName = "clear_alarm") + String clearAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts, @Param("a_details") String details); + + @Procedure(procedureName = "assign_alarm") + String assignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("u_id") UUID userId, @Param("a_ts") long assignTime); + + @Procedure(procedureName = "unassign_alarm") + String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index eeb5c98957..d6bc52b3bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -15,25 +15,32 @@ */ package org.thingsboard.server.dao.sql.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.hibernate.dialect.Dialect; -import org.hibernate.type.UUIDCharType; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -41,19 +48,21 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.EntityAlarmEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.query.AlarmQueryRepository; import org.thingsboard.server.dao.util.SqlDao; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -222,8 +231,171 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public AlarmInfo acknowledgeAlarm(TenantId tenantId, AlarmId id) { - return DaoUtil.getData(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), System.currentTimeMillis())); + public AlarmApiCallResult createOrUpdateActiveAlarm(CreateOrUpdateActiveAlarmRequest request, boolean alarmCreationEnabled) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm( + request.getTenantId().getId(), + request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID, + UUID.randomUUID(), + System.currentTimeMillis(), + request.getOriginator().getId(), + request.getOriginator().getEntityType().ordinal(), + request.getType(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap), + alarmCreationEnabled + )); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.updateAlarm( + request.getTenantId().getId(), + request.getAlarmId().getId(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap) + )); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) { + return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { + return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, getDetailsAsString(details))); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId id, UserId assigneeId, long assignTime) { + return toAlarmApiResult(alarmRepository.assignAlarm(tenantId.getId(), id.getId(), assigneeId.getId(), assignTime)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId id, long unassignTime) { + return toAlarmApiResult(alarmRepository.unassignAlarm(tenantId.getId(), id.getId(), unassignTime)); + } + + @NotNull + private static String getPropagationTypes(AlarmPropagationInfo ap) { + String propagateRelationTypes; + if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { + propagateRelationTypes = String.join(",", ap.getPropagateRelationTypes()); + } else { + propagateRelationTypes = ""; + } + return propagateRelationTypes; + } + + private static AlarmPropagationInfo getSafePropagationInfo(AlarmPropagationInfo ap) { + return ap != null ? ap : AlarmPropagationInfo.EMPTY; + } + + private static String getDetailsAsString(JsonNode details) { + var detailsStr = JacksonUtil.toString(details); + if (StringUtils.isEmpty(detailsStr)) { + detailsStr = "{}"; + } + return detailsStr; + } + + private AlarmApiCallResult toAlarmApiResult(String str) { + var json = JacksonUtil.toJsonNode(str); + var result = AlarmApiCallResult.builder(); + boolean success = json.get("success").asBoolean(); + result.successful(success); + if (success) { + boolean modified = false; + boolean created = false; + boolean cleared = false; + if (json.has("modified")) { + modified = json.get("modified").asBoolean(); + } + + if (json.has("created")) { + created = json.get("created").asBoolean(); + } + + if (json.has("cleared")) { + cleared = json.get("cleared").asBoolean(); + } + result.created(created); + result.cleared(cleared); + result.modified(created || cleared || modified); + if (json.has("alarm") && !json.get("alarm").isNull()) { + result.alarm(toAlarmInfo(json.get("alarm"))); + } + if (json.has("old") && !json.get("old").isNull()) { + result.old(toAlarm(json.get("old"))); + } + } + return result.build(); + } + + private AlarmInfo toAlarmInfo(JsonNode json) { + AlarmInfo alarmInfo = new AlarmInfo(toAlarm(json)); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY).ifPresent(alarmInfo::setOriginatorName); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY).ifPresent(alarmInfo::setOriginatorLabel); + if (alarmInfo.getAssigneeId() != null) { + var assigneeBuilder = AlarmAssignee.builder().id(alarmInfo.getAssigneeId()); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY).ifPresent(assigneeBuilder::firstName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY).ifPresent(assigneeBuilder::lastName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY).ifPresent(assigneeBuilder::email); + alarmInfo.setAssignee(assigneeBuilder.build()); + } + return alarmInfo; + } + + private Alarm toAlarm(JsonNode json) { + Alarm alarm = new Alarm(new AlarmId(UUID.fromString(json.get(ModelConstants.ID_PROPERTY).asText()))); + alarm.setCreatedTime(json.get(ModelConstants.CREATED_TIME_PROPERTY).asLong()); + getSafe(json, ModelConstants.TENANT_ID_COLUMN).ifPresent(s -> alarm.setTenantId(TenantId.fromUUID(UUID.fromString(s)))); + getSafe(json, ModelConstants.CUSTOMER_ID_PROPERTY).ifPresent(s -> alarm.setCustomerId(new CustomerId(UUID.fromString(s)))); + getSafe(json, ModelConstants.ASSIGNEE_ID_PROPERTY).ifPresent(s -> alarm.setAssigneeId(new UserId(UUID.fromString(s)))); + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid( + json.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY).asInt(), + json.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY).asText())); + getSafe(json, ModelConstants.ALARM_TYPE_PROPERTY).ifPresent(alarm::setType); + getSafe(json, ModelConstants.ALARM_SEVERITY_PROPERTY).map(AlarmSeverity::valueOf).ifPresent(alarm::setSeverity); + alarm.setAcknowledged(json.get(ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY).asBoolean()); + alarm.setCleared(json.get(ModelConstants.ALARM_CLEARED_PROPERTY).asBoolean()); + alarm.setPropagate(json.get(ModelConstants.ALARM_PROPAGATE_PROPERTY).asBoolean()); + alarm.setPropagateToOwner(json.get(ModelConstants.ALARM_PROPAGATE_TO_OWNER_PROPERTY).asBoolean()); + alarm.setPropagateToTenant(json.get(ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY).asBoolean()); + alarm.setStartTs(json.get(ModelConstants.ALARM_START_TS_PROPERTY).asLong()); + alarm.setEndTs(json.get(ModelConstants.ALARM_END_TS_PROPERTY).asLong()); + alarm.setAckTs(json.get(ModelConstants.ALARM_ACK_TS_PROPERTY).asLong()); + alarm.setClearTs(json.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY).asLong()); + alarm.setAssignTs(json.get(ModelConstants.ALARM_ASSIGN_TS_PROPERTY).asLong()); + getSafe(json, ModelConstants.ALARM_DETAILS_PROPERTY).map(JacksonUtil::toJsonNode).ifPresent(alarm::setDetails); + alarm.setPropagateRelationTypes(getSafe(json, ModelConstants.ALARM_PROPAGATE_RELATION_TYPES).filter(StringUtils::isNoneEmpty) + .map(s -> Arrays.asList(s.split(","))).orElse(Collections.emptyList())); + return alarm; + } + + private static Optional getSafe(JsonNode json, String fieldName) { + if (json.has(fieldName)) { + var element = json.get(fieldName); + if (element.isNull() || !element.isTextual()) { + return Optional.empty(); + } else { + return Optional.of(element.asText()); + } + } else { + return Optional.empty(); + } } @Override diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index f6a4cac6b5..cdcaef0ab3 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,8 +20,8 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time_active - ON alarm USING btree (tenant_id, type, created_time DESC) WHERE cleared = false; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type, created_time DESC) WHERE cleared = false; CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index c752d35005..1eab033b41 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -800,7 +800,7 @@ CREATE TABLE IF NOT EXISTS user_settings ( CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); -DROP VIEW IF EXISTS alarm_info; +DROP VIEW IF EXISTS alarm_info CASCADE; CREATE VIEW alarm_info AS SELECT a.*, (CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK' @@ -833,12 +833,211 @@ u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email FROM alarm a LEFT JOIN tb_user u ON u.id = a.assignee_id; +CREATE OR REPLACE FUNCTION create_or_update_active_alarm( + t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint, + a_o_id uuid, a_o_type integer, a_type varchar, + a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar, + a_creation_enabled boolean) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid; + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type ORDER BY a.start_ts DESC FOR UPDATE; + IF existing.id IS NULL OR existing.cleared IS TRUE THEN + IF a_creation_enabled = FALSE THEN + RETURN json_build_object('success', false)::text; + END IF; + IF c_id = null_id THEN + c_id = NULL; + end if; + INSERT INTO alarm + (tenant_id, customer_id, id, created_time, + originator_id, originator_type, type, + severity, start_ts, end_ts, + additional_info, + propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types, + acknowledged, ack_ts, + cleared, clear_ts, + assignee_id, assign_ts) + VALUES + (t_id, c_id, a_id, a_created_ts, + a_o_id, a_o_type, a_type, + a_severity, a_start_ts, a_end_ts, + a_details, + a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types, + false, 0, false, 0, NULL, 0); + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text; + ELSE + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = existing.id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text; + END IF; + END IF; +END +$$; + +DROP FUNCTION IF EXISTS update_alarm; +CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = a_id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text; + END IF; +END +$$; + DROP FUNCTION IF EXISTS acknowledge_alarm; CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint) -RETURNS alarm_info LANGUAGE plpgsql -AS $$ + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; BEGIN -UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id and a.acknowledged = false; -RETURN (SELECT a FROM alarm_info a WHERE a.tenant_id = t_id AND id = a_id); -END; + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + + IF NOT (existing.acknowledged) THEN + modified = TRUE; + UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END $$; + +DROP FUNCTION IF EXISTS clear_alarm; +CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + cleared boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF NOT(existing.cleared) THEN + cleared = TRUE; + UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS assign_alarm; +CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS unassign_alarm; +CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NOT NULL THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; \ No newline at end of file diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index 22ddd57e66..897f77a0ce 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.Arrays; @@ -188,7 +189,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); - alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); + alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()); created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -257,7 +258,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(tenantUser); - AlarmOperationResult assignmentResult = alarmService.assignAlarm(tenantId, created.getId(), tenantUser.getId(), ts); + AlarmApiCallResult assignmentResult = alarmService.assignAlarm(tenantId, created.getId(), tenantUser.getId(), ts); created = assignmentResult.getAlarm(); PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -697,7 +698,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); - created = alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get().getAlarm(); + created = new Alarm(alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm()); pageLink.setPage(0); pageLink.setPageSize(10); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index f0fb9cd97d..389510d7d2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -17,23 +17,35 @@ package org.thingsboard.server.dao.sql.alarm; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; +import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; 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.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Created by Valerii Sosliuk on 5/21/2017. @@ -48,35 +60,232 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void testFindLatestByOriginatorAndType() throws ExecutionException, InterruptedException, TimeoutException { log.info("Current system time in millis = {}", System.currentTimeMillis()); - UUID tenantId = UUID.fromString("d4b68f40-3e96-11e7-a884-898080180d6b"); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); UUID originator2Id = UUID.fromString("d4b68f42-3e96-11e7-a884-898080180d6b"); UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); UUID alarm2Id = UUID.fromString("d4b68f44-3e96-11e7-a884-898080180d6b"); UUID alarm3Id = UUID.fromString("d4b68f45-3e96-11e7-a884-898080180d6b"); - int alarmCountBeforeSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + // The find method does not filter by tenant. It is just using the tenantId for rate limits if any. + var alarmsBeforeSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountBeforeSave = alarmsBeforeSave.size(); + saveAlarm(alarm1Id, tenantId.getId(), originator1Id, "TEST_ALARM"); //The timestamp of the startTime should be different in order for test to always work Thread.sleep(1); - saveAlarm(alarm2Id, tenantId, originator1Id, "TEST_ALARM"); - saveAlarm(alarm3Id, tenantId, originator2Id, "TEST_ALARM"); - int alarmCountAfterSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - assertEquals(3, alarmCountAfterSave - alarmCountBeforeSave); + saveAlarm(alarm2Id, tenantId.getId(), originator1Id, "TEST_ALARM"); + saveAlarm(alarm3Id, tenantId.getId(), originator2Id, "TEST_ALARM"); + var alarmsAfterSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountAfterSave = alarmsAfterSave.size(); + int diff = alarmCountAfterSave - alarmCountBeforeSave; + if (diff != 3) { + System.out.println("test"); + } + assertEquals(3, diff); ListenableFuture future = alarmDao - .findLatestByOriginatorAndTypeAsync(TenantId.fromUUID(tenantId), new DeviceId(originator1Id), "TEST_ALARM"); + .findLatestByOriginatorAndTypeAsync(tenantId, new DeviceId(originator1Id), "TEST_ALARM"); Alarm alarm = future.get(30, TimeUnit.SECONDS); assertNotNull(alarm); assertEquals(alarm2Id, alarm.getId().getId()); } + @Test + public void createOrUpdateActiveAlarm() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + CreateOrUpdateActiveAlarmRequest request = CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + UUID newAlarmId = result.getAlarm().getUuidId(); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + request = CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, result.getAlarm().getUuidId()); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE2") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + } + + @Test + public void testCantCreateAlarmIfCreateIsDisabled() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + CreateOrUpdateActiveAlarmRequest request = CreateOrUpdateActiveAlarmRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, false); + assertFalse(result.isSuccessful()); + } + @Test public void testAckAlarmProcedure() { - UUID tenantId = UUID.fromString("d4b68f40-3e96-11e7-a884-898080180d6b"); + UUID tenantId = UUID.randomUUID(); UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); - AlarmInfo alarmInfo = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId()); - assertNotNull(alarmInfo); + long ackTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs + 1); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + } + + @Test + public void testClearAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long clearTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs, null); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCleared()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isCleared()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + } + + @Test + public void testAssignAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarmId = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + UserId userId1 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d7b")); + UserId userId2 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d8b")); + Alarm alarm = saveAlarm(alarmId, tenantId, originator1Id, "TEST_ALARM"); + long assignTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId2, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs + 1, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId2, result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); } private Alarm saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { @@ -90,6 +299,7 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { alarm.setEndTs(System.currentTimeMillis()); alarm.setAcknowledged(false); alarm.setCleared(false); + alarm.setDetails(JacksonUtil.newObjectNode().put("a", UUID.randomUUID().toString()).set("b", JacksonUtil.newObjectNode().put("a", "[}/.`1321421!@@$$(%&&$"))); return alarmDao.save(TenantId.fromUUID(tenantId), alarm); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index c111a498c3..7927843900 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.CreateOrUpdateActiveAlarmRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -32,6 +34,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.Collection; @@ -41,19 +44,45 @@ import java.util.Collection; */ public interface RuleEngineAlarmService { - Alarm createOrUpdateAlarm(Alarm alarm); + /* + * New API, since 3.5. + */ - Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(CreateOrUpdateActiveAlarmRequest request); + /** + * Designed to update existing alarm. Accepts only part of the alarm fields. + */ + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5", forRemoval = true) + Alarm createOrUpdateAlarm(Alarm alarm); + + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); - AlarmOperationResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); - - AlarmOperationResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + // Other API + Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); @@ -63,7 +92,7 @@ public interface RuleEngineAlarmService { AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); - default ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId){ + default ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { return Futures.immediateFuture(findAlarmInfoById(tenantId, alarmId)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 0be8fa5dd3..0cd7b77fd6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; @Slf4j @RuleNode( @@ -74,22 +75,14 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); - return Futures.transformAsync(asyncDetails, details -> { + return Futures.transform(asyncDetails, details -> { ctx.logJsEvalResponse(); - ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), details, System.currentTimeMillis()); - return Futures.transformAsync(clearFuture, cleared -> { - ListenableFuture savedAlarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); - return Futures.transformAsync(savedAlarmFuture, savedAlarm -> { - if (cleared && savedAlarm != null) { - alarm.setDetails(savedAlarm.getDetails()); - alarm.setEndTs(savedAlarm.getEndTs()); - alarm.setClearTs(savedAlarm.getClearTs()); - } - //TODO: remove and return the alarm from a DB call. - alarm.setCleared(true); - return Futures.immediateFuture(new TbAlarmResult(false, false, true, alarm)); - }, ctx.getDbCallbackExecutor()); - }, ctx.getDbCallbackExecutor()); + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); + if (result.isSuccessful()) { + return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); + } else { + return new TbAlarmResult(false, false, false, alarm); + } }, ctx.getDbCallbackExecutor()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index e4bb84d839..3e7bb48d65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -57,7 +57,7 @@ import java.util.List; ) public class TbCreateAlarmNode extends TbAbstractAlarmNode { - private static ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = new ObjectMapper(); private List relationTypes; private AlarmSeverity notDynamicAlarmSeverity; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 2f9da31c8b..49d1bd7877 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -17,12 +17,10 @@ package org.thingsboard.rule.engine.profile; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; @@ -30,16 +28,14 @@ import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.alarm.AlarmOperationResult; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import java.util.ArrayList; import java.util.Comparator; @@ -127,16 +123,12 @@ class AlarmState { for (AlarmRuleState state : createRulesSortedBySeverityDesc) { stateUpdate = clearAlarmState(stateUpdate, state); } - ListenableFuture alarmClearOperationResult = ctx.getAlarmService().clearAlarmForResult( - ctx.getTenantId(), currentAlarm.getId(), createDetails(clearState), System.currentTimeMillis() + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearState) ); - DonAsynchron.withCallback(alarmClearOperationResult, - result -> { - pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); - }, - throwable -> { - throw new RuntimeException(throwable); - }); + if (result.isCleared()) { + pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); + } currentAlarm = null; } else if (AlarmEvalResult.FALSE.equals(evalResult)) { stateUpdate = clearAlarmState(stateUpdate, clearState);