From 3f60d1c0fb3b1cd639eaf268c8dba95150c0fac7 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 18 Jul 2025 15:00:10 +0300 Subject: [PATCH 1/2] Improve error messages when testing scripts --- .../controller/CalculatedFieldController.java | 109 ++++++------ .../controller/RuleChainController.java | 163 +++++++----------- 2 files changed, 117 insertions(+), 155 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 2dcb32cf39..5945355ef8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -17,19 +17,21 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; 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.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; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; @@ -41,7 +43,6 @@ 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.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; @@ -49,17 +50,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.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.CalculatedFieldScriptEngine; 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; @@ -100,30 +98,30 @@ public class CalculatedFieldController extends BaseController { private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" - + MARKDOWN_CODE_BLOCK_START - + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + - " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + - " }\n" + - "}" - + MARKDOWN_CODE_BLOCK_END - + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + @@ -133,13 +131,12 @@ public class CalculatedFieldController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/calculatedField") public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.") @RequestBody CalculatedField calculatedField) throws Exception { calculatedField.setTenantId(getTenantId()); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); - checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser()); + checkReferencedEntities(calculatedField.getConfiguration()); return tbCalculatedFieldService.save(calculatedField, getCurrentUser()); } @@ -147,8 +144,7 @@ public class CalculatedFieldController extends BaseController { notes = "Fetch the Calculated Field object based on the provided Calculated Field Id." ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/calculatedField/{calculatedFieldId}") public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -162,8 +158,7 @@ public class CalculatedFieldController extends BaseController { notes = "Fetch the Calculated Fields based on the provided Entity Id." ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) public PageData getCalculatedFieldsByEntityId( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @@ -182,8 +177,8 @@ public class CalculatedFieldController extends BaseController { @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping("/calculatedField/{calculatedFieldId}") + @ResponseStatus(HttpStatus.OK) public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -196,8 +191,7 @@ public class CalculatedFieldController extends BaseController { notes = "Gets latest calculated field debug event for specified calculated field id. " + "Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}/debug", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/calculatedField/{calculatedFieldId}/debug") public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -212,15 +206,13 @@ public class CalculatedFieldController extends BaseController { @ApiOperation(value = "Test Script expression", notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/testScript", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/calculatedField/testScript") public JsonNode testScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") @RequestBody JsonNode inputParams) { String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { - }), + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), Collections.emptyMap() ); @@ -231,12 +223,13 @@ public class CalculatedFieldController extends BaseController { String output = ""; String errorText = ""; + CalculatedFieldTbelScriptEngine engine = null; try { if (tbelInvokeService == null) { throw new IllegalArgumentException("TBEL script engine is disabled!"); } - CalculatedFieldScriptEngine calculatedFieldScriptEngine = new CalculatedFieldTbelScriptEngine( + engine = new CalculatedFieldTbelScriptEngine( getTenantId(), tbelInvokeService, expression, @@ -254,17 +247,20 @@ public class CalculatedFieldController extends BaseController { } } - JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); output = JacksonUtil.toString(json); } catch (Exception e) { log.error("Error evaluating expression", e); - errorText = e.getMessage(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } } - - ObjectNode result = JacksonUtil.newObjectNode(); - result.put("output", output); - result.put("error", errorText); - return result; + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); } private long getLatestTimestamp(Map arguments) { @@ -281,7 +277,7 @@ public class CalculatedFieldController extends BaseController { return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; } - private & HasTenantId, I extends EntityId> void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig, SecurityUser user) throws ThingsboardException { + private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); @@ -290,8 +286,7 @@ public class CalculatedFieldController extends BaseController { return; } case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> - throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 1674db8755..24986fcbee 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -23,16 +22,19 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; 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.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; @@ -155,9 +157,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain (getRuleChainById)", notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}") public RuleChain getRuleChainById( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -169,9 +170,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/output/labels") public Set getRuleChainOutputLabels( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -184,9 +184,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels/usage", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/output/labels/usage") public List getRuleChainOutputLabelsUsage( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -198,9 +197,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain (getRuleChainById)", notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/metadata") public RuleChainMetaData getRuleChainMetaData( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -218,9 +216,8 @@ public class RuleChainController extends BaseController { "\n\n" + RULE_CHAIN_DESCRIPTION + "Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain") public RuleChain saveRuleChain( @Parameter(description = "A JSON value representing the rule chain.") @RequestBody RuleChain ruleChain) throws Exception { @@ -232,9 +229,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Create Default Rule Chain", notes = "Create rule chain from template, based on the specified name in the request. " + "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/device/default") public RuleChain saveRuleChain( @Parameter(description = "A JSON value representing the request.") @RequestBody DefaultRuleChainCreateRequest request) throws Exception { @@ -245,9 +241,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)", notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/{ruleChainId}/root") public RuleChain setRootRuleChain( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -259,9 +254,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Update Rule Chain Metadata", notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/metadata") public RuleChainMetaData saveRuleChainMetaData( @Parameter(description = "A JSON value representing the rule chain metadata.") @RequestBody RuleChainMetaData ruleChainMetaData, @@ -284,8 +278,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chains (getRuleChains)", notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) public PageData getRuleChains( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -302,7 +295,7 @@ public class RuleChainController extends BaseController { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); RuleChainType type = RuleChainType.CORE; - if (typeStr != null && typeStr.trim().length() > 0) { + if (StringUtils.isNotEmpty(typeStr)) { type = RuleChainType.valueOf(typeStr); } return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); @@ -311,9 +304,9 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Delete rule chain (deleteRuleChain)", notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " + "Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) - @ResponseStatus(value = HttpStatus.OK) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/ruleChain/{ruleChainId}") + @ResponseStatus(HttpStatus.OK) public void deleteRuleChain( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -326,9 +319,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)", notes = "Gets the input message from the debug events for specified Rule Chain Id. " + "Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleNode/{ruleNodeId}/debugIn") public JsonNode getLatestRuleNodeDebugInput( @Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION) @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { @@ -343,8 +335,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Is TBEL script executor enabled", notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/ruleChain/tbelEnabled") public Boolean isTbelEnabled() { return tbelEnabled; } @@ -352,13 +343,12 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Test Script function", notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChain/testScript") public JsonNode testScript( @Parameter(description = "Script language: JS or TBEL") @RequestParam(required = false) ScriptLanguage scriptLang, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") - @RequestBody JsonNode inputParams) throws ThingsboardException, JsonProcessingException { + @RequestBody JsonNode inputParams) { String script = inputParams.get("script").asText(); String scriptType = inputParams.get("scriptType").asText(); JsonNode argNamesJson = inputParams.get("argNames"); @@ -366,8 +356,7 @@ public class RuleChainController extends BaseController { String data = inputParams.get("msg").asText(); JsonNode metadataJson = inputParams.get("metadata"); - Map metadata = JacksonUtil.convertValue(metadataJson, new TypeReference>() { - }); + Map metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<>() {}); String msgType = inputParams.get("msgType").asText(); String output = ""; String errorText = ""; @@ -384,55 +373,40 @@ public class RuleChainController extends BaseController { } engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); } - TbMsg inMsg = TbMsg.newMsg() + + var inMsg = TbMsg.newMsg() .type(msgType) .copyMetaData(new TbMsgMetaData(metadata)) .dataType(TbMsgDataType.JSON) .data(data) .build(); - switch (scriptType) { - case "update": - output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "generate": - output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "filter": - boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = Boolean.toString(result); - break; - case "switch": - Set states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(states); - break; - case "json": - JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - break; - case "string": - output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - break; - default: - throw new IllegalArgumentException("Unsupported script type: " + scriptType); - } + + output = switch (scriptType) { + case "update" -> msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "generate" -> msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "filter" -> Boolean.toString(engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "switch" -> JacksonUtil.toString(engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "json" -> JacksonUtil.toString(engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "string" -> engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + default -> throw new IllegalArgumentException("Unsupported script type: " + scriptType); + }; } catch (Exception e) { log.error("Error evaluating JS function", e); - errorText = e.getMessage(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); } finally { if (engine != null) { engine.destroy(); } } - ObjectNode result = JacksonUtil.newObjectNode(); - result.put("output", output); - result.put("error", errorText); - return result; + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); } @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/ruleChains/export", params = {"limit"}) public RuleChainData exportRuleChains( @Parameter(description = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { @@ -443,8 +417,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChains/import") public List importRuleChains( @Parameter(description = "A JSON value representing the rule chains.") @RequestBody RuleChainData ruleChainData, @@ -454,12 +427,12 @@ public class RuleChainController extends BaseController { return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration); } - private String msgToOutput(TbMsg msg) throws Exception { + private String msgToOutput(TbMsg msg) { JsonNode resultNode = convertMsgToOut(msg); return JacksonUtil.toString(resultNode); } - private String msgToOutput(List msgs) throws Exception { + private String msgToOutput(List msgs) { JsonNode resultNode; if (msgs.size() > 1) { resultNode = JacksonUtil.newArrayNode(); @@ -473,7 +446,7 @@ public class RuleChainController extends BaseController { return JacksonUtil.toString(resultNode); } - private JsonNode convertMsgToOut(TbMsg msg) throws Exception { + private JsonNode convertMsgToOut(TbMsg msg) { ObjectNode msgData = JacksonUtil.newObjectNode(); if (!StringUtils.isEmpty(msg.getData())) { msgData.set("msg", JacksonUtil.toJsonNode(msg.getData())); @@ -492,8 +465,7 @@ public class RuleChainController extends BaseController { "Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " + "\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/edge/{edgeId}/ruleChain/{ruleChainId}") public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); @@ -514,8 +486,7 @@ public class RuleChainController extends BaseController { EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + "Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping("/edge/{edgeId}/ruleChain/{ruleChainId}") public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); @@ -530,9 +501,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) public PageData getEdgeRuleChains( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -557,9 +527,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " + "Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/{ruleChainId}/edgeTemplateRoot") public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -572,8 +541,7 @@ public class RuleChainController extends BaseController { notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " + "Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChain/{ruleChainId}/autoAssignToEdge") public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -586,8 +554,7 @@ public class RuleChainController extends BaseController { notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " + "Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping("/ruleChain/{ruleChainId}/autoAssignToEdge") public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -599,9 +566,8 @@ public class RuleChainController extends BaseController { // TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign @ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)", notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/autoAssignToEdgeRuleChains") public List getAutoAssignToEdgeRuleChains() throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List result = new ArrayList<>(); @@ -612,4 +578,5 @@ public class RuleChainController extends BaseController { } return checkNotNull(result); } + } From f8784fa48f64eb71a2654c714333b453c31b16cc Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Fri, 18 Jul 2025 15:03:30 +0300 Subject: [PATCH 2/2] Fix blank check --- .../org/thingsboard/server/controller/RuleChainController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 24986fcbee..efe3e61893 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -295,7 +295,7 @@ public class RuleChainController extends BaseController { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); RuleChainType type = RuleChainType.CORE; - if (StringUtils.isNotEmpty(typeStr)) { + if (StringUtils.isNotBlank(typeStr)) { type = RuleChainType.valueOf(typeStr); } return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink));