From a63ed4fa6cc2e38b855d0f1ed631addd238f359d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 27 Aug 2024 13:57:32 +0300 Subject: [PATCH] Versioning for rule chain metadata --- .../controller/RuleChainControllerTest.java | 43 +++++++++++++++++++ .../EntityVersionMismatchException.java | 6 +++ .../common/data/rule/RuleChainMetaData.java | 6 ++- .../data/sync/ie/RuleChainExportData.java | 2 +- .../server/dao/rule/BaseRuleChainService.java | 4 ++ .../server/dao/sql/JpaAbstractDao.java | 2 +- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 949dd4a781..0aca783e67 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -30,6 +30,8 @@ import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbCreateAlarmNode; import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode; +import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode; import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration; import org.thingsboard.server.common.data.StringUtils; @@ -356,6 +358,47 @@ public class RuleChainControllerTest extends AbstractControllerTest { assertThat(error).contains("alarmType is malformed"); } + @Test + public void testSaveRuleChainWithOutdatedVersion() throws Exception { + RuleChain ruleChain = createRuleChain("Rule chain with invalid nodes"); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChain.getId()); + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("Test"); + ruleNode.setType(TbMsgGeneratorNode.class.getName()); + TbMsgGeneratorNodeConfiguration config = new TbMsgGeneratorNodeConfiguration(); + ruleNode.setConfiguration(JacksonUtil.valueToTree(config)); + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class); + assertThat(ruleChainMetaData.getVersion()).isEqualTo(2); // in this case when saving metadata, rule chain will also be updated + + ruleChain = doGet("/api/ruleChain/" + ruleChain.getId(), RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(2); + + ruleChain.setName("Updated"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + assertThat(ruleChain.getVersion()).isEqualTo(3); + + ruleChain.setVersion(1L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isConflict()); + ruleChainMetaData.setVersion(1L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isConflict()); + + ruleChainMetaData.setVersion(3L); + doPost("/api/ruleChain/metadata", ruleChainMetaData) + .andExpect(status().isOk()); + ruleChain.setVersion(3L); + doPost("/api/ruleChain", ruleChain) + .andExpect(status().isOk()); + } + private RuleChain createRuleChain(String name) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(name); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java index 310c6bccf7..e6ed0b824a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/EntityVersionMismatchException.java @@ -15,10 +15,16 @@ */ package org.thingsboard.server.common.data.exception; +import org.thingsboard.server.common.data.EntityType; + public class EntityVersionMismatchException extends RuntimeException { public EntityVersionMismatchException(String message, Throwable cause) { super(message, cause); } + public EntityVersionMismatchException(EntityType entityType, Throwable cause) { + this((entityType != null ? entityType.getNormalName() : "Entity") + " was already changed by someone else", cause); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index eb18e28c8c..0ccbd1d34a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.RuleChainId; import java.util.ArrayList; @@ -27,11 +28,14 @@ import java.util.List; */ @Schema @Data -public class RuleChainMetaData { +public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Rule Chain Id.", accessMode = Schema.AccessMode.READ_ONLY) private RuleChainId ruleChainId; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Version of the Rule Chain") + private Long version; + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java index 98eaff96c9..cccffd6fed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/RuleChainExportData.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; public class RuleChainExportData extends EntityExportData { @JsonProperty(index = 3) - @JsonIgnoreProperties("ruleChainId") + @JsonIgnoreProperties({"ruleChainId", "version"}) private RuleChainMetaData metaData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index fea3980446..68bb185e5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -174,6 +175,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -298,6 +301,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); + ruleChainMetaData.setVersion(ruleChain.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 801898386e..a7d49e244b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -79,7 +79,7 @@ public abstract class JpaAbstractDao, D> try { entity = doSave(entity, isNew, flush); } catch (OptimisticLockException e) { - throw new EntityVersionMismatchException((getEntityType() != null ? getEntityType().getNormalName() : "Entity") + " was already changed by someone else", e); + throw new EntityVersionMismatchException(getEntityType(), e); } return DaoUtil.getData(entity); }