Browse Source

Refactor alarm rule controller to remove cross-controller dependency

Move test script execution logic from controllers to TbCalculatedFieldService,
eliminating the dependency of AlarmRuleController on CalculatedFieldController.
Simplify entity type filtering in getAlarmRules. Add AlarmRuleControllerTest
covering all endpoints.
pull/15326/head
Viacheslav Klimov 2 months ago
parent
commit
253f70d789
Failed to extract signature
  1. 86
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  2. 91
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  3. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  5. 420
      application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java

86
application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java

@ -15,15 +15,10 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
@ -35,11 +30,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
@ -49,6 +39,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldFilter;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -60,23 +51,17 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.CalculatedFieldController.TIMEOUT;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
@ -93,13 +78,10 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AlarmRuleController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final CalculatedFieldController calculatedFieldController;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String ALARM_RULE_ID = "alarmRuleId";
@ -130,7 +112,7 @@ public class AlarmRuleController extends BaseController {
alarmRuleDefinition.setTenantId(getTenantId());
checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField();
calculatedFieldController.checkReferencedEntities(calculatedField.getConfiguration());
checkReferencedEntities(calculatedField.getConfiguration());
CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser());
return AlarmRuleDefinition.fromCalculatedField(saved);
}
@ -188,12 +170,10 @@ public class AlarmRuleController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
Set<CalculatedFieldType> types = EnumSet.of(CalculatedFieldType.ALARM);
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream()
.filter(entry -> CollectionUtils.containsAny(entry.getValue(), types))
.filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
} else {
@ -201,7 +181,7 @@ public class AlarmRuleController extends BaseController {
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.types(types)
.types(EnumSet.of(CalculatedFieldType.ALARM))
.entityTypes(entityTypes)
.entityIds(entities)
.build();
@ -262,57 +242,21 @@ public class AlarmRuleController extends BaseController {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null);
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
getTenantId(),
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, CalculatedFieldController.getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType refEntityType = referencedEntityId.getEntityType();
switch (refEntityType) {
case TENANT -> {
return;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'.");
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException {

91
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
@ -24,10 +23,7 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
@ -40,13 +36,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -65,21 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
@ -98,17 +81,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class CalculatedFieldController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String CALCULATED_FIELD_ID = "calculatedFieldId";
static final int TIMEOUT = 20;
static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
@ -305,75 +284,11 @@ public class CalculatedFieldController extends BaseController {
@PostMapping("/calculatedField/testScript")
public JsonNode testCalculatedFieldScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
getTenantId(),
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
static long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
@RequestBody JsonNode inputParams) throws ThingsboardException {
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType refEntityType = referencedEntityId.getEntityType();

92
application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java

@ -15,10 +15,22 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -31,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@ -42,8 +60,13 @@ import java.util.Set;
@RequiredArgsConstructor
public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService {
private static final int TIMEOUT = 20;
private final CalculatedFieldService calculatedFieldService;
@Autowired(required = false)
private TbelInvokeService tbelInvokeService;
@Override
public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException {
ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
@ -89,6 +112,75 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
}
}
@Override
public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
tenantId,
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private static long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
}
private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) {
if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) {
throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited.");

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -35,4 +36,6 @@ public interface TbCalculatedFieldService {
void delete(CalculatedField calculatedField, SecurityUser user);
JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams);
}

420
application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java

@ -0,0 +1,420 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AlarmRuleControllerTest extends AbstractControllerTest {
private Tenant savedTenant;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = saveTenant(tenant);
assertThat(savedTenant).isNotNull();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
deleteTenant(savedTenant.getId());
}
@Test
public void testSaveAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCreatedTime()).isGreaterThan(0);
assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(saved.getEntityId()).isEqualTo(testDevice.getId());
assertThat(saved.getName()).isEqualTo("High Temperature");
assertThat(saved.getConfiguration()).isNotNull();
assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL);
saved.setName("Updated Alarm Rule");
AlarmRuleDefinition updated = saveAlarmRule(saved);
assertThat(updated.getName()).isEqualTo("Updated Alarm Rule");
assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class);
assertThat(fetched).isNotNull();
assertThat(fetched).isEqualTo(saved);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doGet("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRulesByEntityId() throws Exception {
Device device1 = createDevice("Device 1", "1234567890");
Device device2 = createDevice("Device 2", "0987654321");
AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1"));
saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2"));
PageData<AlarmRuleDefinition> result = doGetTypedWithPageLink(
"/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId());
assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1");
}
@Test
public void testGetAlarmRules() throws Exception {
Device device = createDevice("Device A", "1234567890");
AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm"));
DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class);
AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm"));
// All alarm rules
List<AlarmRuleDefinitionInfo> all = getAlarmRules(null, null);
assertThat(all).extracting(AlarmRuleDefinition::getName)
.contains("Device Alarm", "Profile Alarm");
// Filter by entity type: DEVICE
List<AlarmRuleDefinitionInfo> deviceRules = getAlarmRules(EntityType.DEVICE, null);
assertThat(deviceRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Filter by entity type: DEVICE_PROFILE
List<AlarmRuleDefinitionInfo> profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null);
assertThat(profileRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Profile Alarm");
// Filter by specific entity IDs
List<AlarmRuleDefinitionInfo> specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId()));
assertThat(specificRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Verify entity names are populated
AlarmRuleDefinitionInfo deviceInfo = all.stream()
.filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow();
assertThat(deviceInfo.getEntityName()).isEqualTo("Device A");
AlarmRuleDefinitionInfo profileInfo = all.stream()
.filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow();
assertThat(profileInfo.getEntityName()).isEqualTo("Profile A");
}
@Test
public void testGetAlarmRules_textSearch() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm"));
PageData<AlarmRuleDefinitionInfo> result = doGetTypedWithPageLink(
"/api/alarm/rules?textSearch=Temp&",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm");
}
@Test
public void testGetAlarmRuleNames() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm"));
PageData<String> names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(2);
assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder());
assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm");
names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.DESC)));
assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder());
names = getAlarmRuleNames(new PageLink(10, 0,
"Alpha", new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(1);
assertThat(names.getData()).containsOnly("Alpha Alarm");
}
@Test
public void testDeleteAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete"));
assertThat(saved).isNotNull();
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
doGet("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_notFound() throws Exception {
doDelete("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doDelete("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test"));
JsonNode result = doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug", JsonNode.class);
assertThat(result).isNull();
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug")
.andExpect(status().isNotFound());
}
@Test
public void testTestAlarmRuleScript() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.has("output")).isTrue();
assertThat(result.has("error")).isTrue();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("true");
}
@Test
public void testTestAlarmRuleScript_returnsFalse() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("false");
}
@Test
public void testTestAlarmRuleScript_missingExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"arguments": {}
}
""");
doPost("/api/alarm/rule/testScript", request)
.andExpect(status().isBadRequest());
}
@Test
public void testTestAlarmRuleScript_invalidExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "invalid syntax {{{{",
"arguments": {}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isNotEmpty();
}
// --- Helper methods ---
private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) {
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition();
alarmRule.setEntityId(entityId);
alarmRule.setName(name);
alarmRule.setConfigurationVersion(1);
alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode());
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
argument.setDefaultValue("0");
configuration.setArguments(Map.of("temperature", argument));
AlarmRule rule = new AlarmRule();
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
expression.setExpression("return temperature >= 50;");
SimpleAlarmCondition condition = new SimpleAlarmCondition();
condition.setExpression(expression);
rule.setCondition(condition);
configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule));
alarmRule.setConfiguration(configuration);
return alarmRule;
}
private CalculatedField createSimpleCalculatedField(EntityId entityId) {
CalculatedField cf = new CalculatedField();
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("Simple CF");
cf.setConfigurationVersion(1);
cf.setAdditionalInfo(JacksonUtil.newObjectNode());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
config.setArguments(Map.of("T", arg));
config.setExpression("T * 2");
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("result");
config.setOutput(output);
cf.setConfiguration(config);
return cf;
}
private List<AlarmRuleDefinitionInfo> getAlarmRules(EntityType entityType, List<UUID> entities) throws Exception {
StringBuilder url = new StringBuilder("/api/alarm/rules?");
if (entityType != null) {
url.append("entityType=").append(entityType).append("&");
}
if (entities != null) {
url.append("entities=").append(String.join(",",
entities.stream().map(UUID::toString).toList())).append("&");
}
return doGetTypedWithPageLink(url.toString(),
new TypeReference<PageData<AlarmRuleDefinitionInfo>>() {}, new PageLink(10)).getData();
}
private PageData<String> getAlarmRuleNames(PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/alarm/rules/names?",
new TypeReference<PageData<String>>() {}, pageLink);
}
}
Loading…
Cancel
Save