From bb6d04be3b6e9320cb162280fed843ee6d2d12c1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 14:54:02 +0300 Subject: [PATCH 01/42] Logic for handling rule chain updates from edge, including metadata --- .../service/edge/rpc/EdgeGrpcSession.java | 14 +- .../service/edge/rpc/EdgeSyncCursor.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 3 + .../rule/BaseRuleChainProcessor.java | 127 ++++++++++++++++++ .../rule/RuleChainEdgeProcessor.java | 102 +++++++++++++- .../server/edge/AbstractEdgeTest.java | 46 ++----- .../server/edge/AssetEdgeTest.java | 26 ++++ .../server/edge/DashboardEdgeTest.java | 23 ++++ .../server/edge/DeviceEdgeTest.java | 31 +++++ .../server/edge/RuleChainEdgeTest.java | 72 +++++----- .../server/dao/rule/RuleChainService.java | 2 + common/edge-api/src/main/proto/edge.proto | 5 +- .../server/dao/rule/BaseRuleChainService.java | 15 ++- 13 files changed, 390 insertions(+), 78 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index cfb92d0bfd..538a12bd08 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -65,6 +65,8 @@ import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.RequestMsgType; import org.thingsboard.server.gen.edge.v1.ResponseMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; @@ -628,7 +630,7 @@ public final class EdgeGrpcSession implements Closeable { case CUSTOMER: return ctx.getCustomerProcessor().convertCustomerEventToDownlink(edgeEvent); case RULE_CHAIN: - return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent); + return ctx.getRuleChainProcessor().convertRuleChainEventToDownlink(edgeEvent, this.edgeVersion); case RULE_CHAIN_METADATA: return ctx.getRuleChainProcessor().convertRuleChainMetadataEventToDownlink(edgeEvent, this.edgeVersion); case ALARM: @@ -690,6 +692,16 @@ public final class EdgeGrpcSession implements Closeable { result.add(ctx.getAssetProcessor().processAssetMsgFromEdge(edge.getTenantId(), edge, assetUpdateMsg)); } } + if (uplinkMsg.getRuleChainUpdateMsgCount() > 0) { + for (RuleChainUpdateMsg ruleChainUpdateMsg : uplinkMsg.getRuleChainUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMsgFromEdge(edge.getTenantId(), edge, ruleChainUpdateMsg)); + } + } + if (uplinkMsg.getRuleChainMetadataUpdateMsgCount() > 0) { + for (RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg : uplinkMsg.getRuleChainMetadataUpdateMsgList()) { + result.add(ctx.getRuleChainProcessor().processRuleChainMetadataMsgFromEdge(edge.getTenantId(), edge, ruleChainMetadataUpdateMsg)); + } + } if (uplinkMsg.getAlarmUpdateMsgCount() > 0) { for (AlarmUpdateMsg alarmUpdateMsg : uplinkMsg.getAlarmUpdateMsgList()) { result.add(ctx.getAlarmProcessor().processAlarmMsg(edge.getTenantId(), alarmUpdateMsg)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index a39dae3b54..586e1b403a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -53,7 +53,6 @@ public class EdgeSyncCursor { public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { if (fullSync) { fetchers.add(new QueuesEdgeEventFetcher(ctx.getQueueService())); - fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); fetchers.add(new TenantEdgeEventFetcher(ctx.getTenantService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); @@ -64,6 +63,7 @@ public class EdgeSyncCursor { fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } } + fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 328a64efe2..78bd56d90c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -233,6 +233,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected DataValidator entityViewValidator; + @Autowired + protected DataValidator ruleChainValidator; + @Autowired protected EdgeMsgConstructor edgeMsgConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java new file mode 100644 index 0000000000..dc9fa6e14d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -0,0 +1,127 @@ +/** + * 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.service.edge.rpc.processor.rule; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleNodeProto; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +@Slf4j +public class BaseRuleChainProcessor extends BaseEdgeProcessor { + + protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg) { + boolean created = false; + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); + if (ruleChain == null) { + created = true; + ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setCreatedTime(Uuids.unixTimestamp(ruleChainId.getId())); + } + ruleChain.setName(ruleChainUpdateMsg.getName()); + ruleChain.setType(RuleChainType.EDGE); + ruleChain.setDebugMode(ruleChainUpdateMsg.getDebugMode()); + ruleChain.setConfiguration(JacksonUtil.toJsonNode(ruleChainUpdateMsg.getConfiguration())); + + UUID firstRuleNodeUUID = safeGetUUID(ruleChainUpdateMsg.getFirstRuleNodeIdMSB(), ruleChainUpdateMsg.getFirstRuleNodeIdLSB()); + ruleChain.setFirstRuleNodeId(firstRuleNodeUUID != null ? new RuleNodeId(firstRuleNodeUUID) : null); + + ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + if (created) { + ruleChain.setId(ruleChainId); + } + ruleChainService.saveRuleChain(ruleChain, false); + return created; + } + + protected boolean saveOrUpdateRuleChainMetadata(TenantId tenantId, RuleChainId ruleChainId, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) throws IOException { + RuleChainMetaData ruleChainMetadata = new RuleChainMetaData(); + ruleChainMetadata.setRuleChainId(ruleChainId); + ruleChainMetadata.setNodes(parseNodeProtos(ruleChainId, ruleChainMetadataUpdateMsg.getNodesList())); + ruleChainMetadata.setConnections(parseConnectionProtos(ruleChainMetadataUpdateMsg.getConnectionsList())); + ruleChainMetadata.setRuleChainConnections(parseRuleChainConnectionProtos(ruleChainMetadataUpdateMsg.getRuleChainConnectionsList())); + if (ruleChainMetadataUpdateMsg.getFirstNodeIndex() != -1) { + ruleChainMetadata.setFirstNodeIndex(ruleChainMetadataUpdateMsg.getFirstNodeIndex()); + } + if (ruleChainMetadata.getNodes().size() > 0) { + ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity()); + return true; + } + return false; + } + + private List parseNodeProtos(RuleChainId ruleChainId, List nodesList) throws IOException { + List result = new ArrayList<>(); + for (RuleNodeProto proto : nodesList) { + RuleNode ruleNode = new RuleNode(); + RuleNodeId ruleNodeId = new RuleNodeId(new UUID(proto.getIdMSB(), proto.getIdLSB())); + ruleNode.setId(ruleNodeId); + ruleNode.setCreatedTime(Uuids.unixTimestamp(ruleNodeId.getId())); + ruleNode.setRuleChainId(ruleChainId); + ruleNode.setType(proto.getType()); + ruleNode.setName(proto.getName()); + ruleNode.setDebugMode(proto.getDebugMode()); + ruleNode.setConfiguration(JacksonUtil.OBJECT_MAPPER.readTree(proto.getConfiguration())); + ruleNode.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.readTree(proto.getAdditionalInfo())); + result.add(ruleNode); + } + return result; + } + + private List parseConnectionProtos(List connectionsList) { + List result = new ArrayList<>(); + for (org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto proto : connectionsList) { + NodeConnectionInfo info = new NodeConnectionInfo(); + info.setFromIndex(proto.getFromIndex()); + info.setToIndex(proto.getToIndex()); + info.setType(proto.getType()); + result.add(info); + } + return result; + } + + private List parseRuleChainConnectionProtos(List ruleChainConnectionsList) throws IOException { + List result = new ArrayList<>(); + for (org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto proto : ruleChainConnectionsList) { + RuleChainConnectionInfo info = new RuleChainConnectionInfo(); + info.setFromIndex(proto.getFromIndex()); + info.setTargetRuleChainId(new RuleChainId(new UUID(proto.getTargetRuleChainIdMSB(), proto.getTargetRuleChainIdLSB()))); + info.setType(proto.getType()); + info.setAdditionalInfo(JacksonUtil.OBJECT_MAPPER.readTree(proto.getAdditionalInfo())); + result.add(info); + } + return result; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index ba6fc1ef97..ae5e158ba7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -15,29 +15,120 @@ */ package org.thingsboard.server.service.edge.rpc.processor.rule; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY; @Component @Slf4j @TbCoreComponent -public class RuleChainEdgeProcessor extends BaseEdgeProcessor { +public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { + + public ListenableFuture processRuleChainMsgFromEdge(TenantId tenantId, Edge edge, RuleChainUpdateMsg ruleChainUpdateMsg) { + log.trace("[{}] executing processRuleChainMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainUpdateMsg, edge.getName()); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (ruleChainUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + RuleChain ruleChainToDelete = ruleChainService.findRuleChainById(tenantId, ruleChainId); + if (ruleChainToDelete != null) { + ruleChainService.unassignRuleChainFromEdge(tenantId, ruleChainId, edge.getId(), false); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(ruleChainUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed rule chains violated {}", tenantId, ruleChainUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + + public ListenableFuture processRuleChainMetadataMsgFromEdge(TenantId tenantId, Edge edge, RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { + log.trace("[{}] executing processRuleChainMetadataMsgFromEdge [{}] from edge [{}]", tenantId, ruleChainMetadataUpdateMsg, edge.getName()); + RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); + try { + edgeSynchronizationManager.getSync().set(true); + + switch (ruleChainMetadataUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + saveOrUpdateRuleChainMetadata(tenantId, ruleChainId, ruleChainMetadataUpdateMsg); + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(ruleChainMetadataUpdateMsg.getMsgType()); + } + } catch (Exception e) { + String errMsg = String.format("Can't process rule chain metadata update msg %s", ruleChainMetadataUpdateMsg); + log.error(errMsg, e); + return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); + } finally { + edgeSynchronizationManager.getSync().remove(); + } + } + + private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { + boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), ruleChainId); + pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId); + ruleChainService.assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); + } + if (ruleChainUpdateMsg.getRoot()) { + edge.setRootRuleChainId(ruleChainId); + edgeService.saveEdge(edge); + } + } + + private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId) { + try { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); + String ruleChainAsString = JacksonUtil.toString(ruleChain); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, null); + pushEntityEventToRuleEngine(tenantId, ruleChainId, null, TbMsgType.ENTITY_CREATED, ruleChainAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push rule chain action to rule engine: {}", tenantId, ruleChainId, TbMsgType.ENTITY_CREATED.name(), e); + } + } - public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -55,10 +146,13 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainMsgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); + RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainUpdateMsg(ruleChainUpdateMsg) - .build(); + .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg).build(); } break; case DELETED: diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 410348762f..dc1015ef4a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -81,14 +81,12 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.queue.util.DataDecodingEncodingService; @@ -129,36 +127,12 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(26); + edgeImitator.expectMessageAmount(27); edgeImitator.connect(); - requestEdgeRuleChainMetadata(); - verifyEdgeConnectionAndInitialData(); } - private void requestEdgeRuleChainMetadata() throws Exception { - RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); - RuleChainMetadataRequestMsg.Builder builder = RuleChainMetadataRequestMsg.newBuilder() - .setRuleChainIdMSB(rootRuleChainId.getId().getMostSignificantBits()) - .setRuleChainIdLSB(rootRuleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(builder); - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() - .addRuleChainMetadataRequestMsg(builder.build()); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - } - - private RuleChainId getEdgeRootRuleChainId() throws Exception { - List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", - new TypeReference>() {}, new PageLink(100)).getData(); - for (RuleChain edgeRuleChain : edgeRuleChains) { - if (edgeRuleChain.isRoot()) { - return edgeRuleChain.getId(); - } - } - throw new RuntimeException("Root rule chain not found"); - } - @After public void teardownEdgeTest() { try { @@ -230,7 +204,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller UUID ruleChainUUID = validateRuleChains(); - // 1 from request message + // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller (it goes along with RuleChainUpdateMsg) validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages - 4 messages from fetcher - 2 from system level ('mail', 'mailTemplates') and 2 from admin level ('mail', 'mailTemplates') @@ -385,11 +359,17 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) { - Optional ruleChainMetadataUpdateOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class); - Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent()); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get(); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); - UUID ruleChainUUID = new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()); + List ruleChainMetadataUpdateMsgList = edgeImitator.findAllMessagesByType(RuleChainMetadataUpdateMsg.class); + Assert.assertEquals(2, ruleChainMetadataUpdateMsgList.size()); + // metadata create msg + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgCreated = ruleChainMetadataUpdateMsgList.get(0); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgCreated.getMsgType()); + UUID ruleChainUUID = new UUID(ruleChainMetadataUpdateMsgCreated.getRuleChainIdMSB(), ruleChainMetadataUpdateMsgCreated.getRuleChainIdLSB()); + Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); + // metadata update msg + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgUpdated = ruleChainMetadataUpdateMsgList.get(1); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgUpdated.getMsgType()); + ruleChainUUID = new UUID(ruleChainMetadataUpdateMsgUpdated.getRuleChainIdMSB(), ruleChainMetadataUpdateMsgUpdated.getRuleChainIdLSB()); Assert.assertEquals(expectedRuleChainUUID, ruleChainUUID); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java index faf0896b81..e3ad8aaa25 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetEdgeTest.java @@ -168,6 +168,7 @@ public class AssetEdgeTest extends AbstractEdgeTest { public void testSendAssetToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // created asset on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); AssetUpdateMsg.Builder assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -191,6 +192,31 @@ public class AssetEdgeTest extends AbstractEdgeTest { Asset asset = doGet("/api/asset/" + uuid, Asset.class); Assert.assertNotNull(asset); Assert.assertEquals("Asset Edge 2", asset.getName()); + + // updated asset on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + assetUpdateMsgBuilder = AssetUpdateMsg.newBuilder(); + assetUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + assetUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + assetUpdateMsgBuilder.setName("Asset Edge 2 Updated"); + assetUpdateMsgBuilder.setType("test"); + assetUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(assetUpdateMsgBuilder); + uplinkMsgBuilder.addAssetUpdateMsg(assetUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + asset = doGet("/api/asset/" + uuid, Asset.class); + Assert.assertNotNull(asset); + Assert.assertEquals("Asset Edge 2 Updated", asset.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 2d21d30174..7ef3da1fa0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -171,6 +171,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest { public void testSendDashboardToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // create dashboard on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DashboardUpdateMsg.Builder dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -191,6 +192,28 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Dashboard dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); Assert.assertNotNull(dashboard); Assert.assertEquals("Edge Test Dashboard", dashboard.getName()); + + // update dashboard on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + dashboardUpdateMsgBuilder = DashboardUpdateMsg.newBuilder(); + dashboardUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + dashboardUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + dashboardUpdateMsgBuilder.setTitle("Edge Test Dashboard Updated"); + dashboardUpdateMsgBuilder.setConfiguration(""); + dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(dashboardUpdateMsgBuilder); + uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + dashboard = doGet("/api/dashboard/" + uuid, Dashboard.class); + Assert.assertNotNull(dashboard); + Assert.assertEquals("Edge Test Dashboard Updated", dashboard.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 15b15c0be0..42f8d1d272 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -538,6 +538,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { public void testSendDeviceToCloud() throws Exception { UUID uuid = Uuids.timeBased(); + // create device on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); DeviceUpdateMsg.Builder deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); @@ -566,6 +567,36 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device device = doGet("/api/device/" + newDeviceId, Device.class); Assert.assertNotNull(device); Assert.assertEquals("Edge Device 2", device.getName()); + + // update device on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + deviceUpdateMsgBuilder = DeviceUpdateMsg.newBuilder(); + deviceUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + deviceUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + deviceUpdateMsgBuilder.setName("Edge Device 2 Updated"); + deviceUpdateMsgBuilder.setType("test"); + deviceUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg); + latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage; + Assert.assertEquals(uuid.getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); + Assert.assertEquals(uuid.getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + + newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); + + device = doGet("/api/device/" + newDeviceId, Device.class); + Assert.assertNotNull(device); + Assert.assertEquals("Edge Device 2 Updated", device.getName()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 83b2370cf0..4be82e7d8b 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.edge; -import com.google.protobuf.AbstractMessage; +import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; @@ -29,11 +29,10 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataRequestMsg; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import java.util.ArrayList; import java.util.Collections; @@ -49,7 +48,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { @Test public void testRuleChains() throws Exception { // create rule chain - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(4); RuleChain ruleChain = new RuleChain(); ruleChain.setName("Edge Test Rule Chain"); ruleChain.setType(RuleChainType.EDGE); @@ -67,8 +66,6 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), savedRuleChain.getUuidId().getLeastSignificantBits()); Assert.assertEquals(ruleChainUpdateMsg.getName(), savedRuleChain.getName()); - testRuleChainMetadataRequestMsg(savedRuleChain.getId()); - // unassign rule chain from edge edgeImitator.expectMessageAmount(1); doDelete("/api/edge/" + edge.getUuidId() @@ -89,55 +86,56 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { } @Test - public void testSendRuleChainMetadataRequestToCloud() throws Exception { - RuleChainId edgeRootRuleChainId = edge.getRootRuleChainId(); + public void testRuleChainToCloud() throws Exception { + UUID uuid = Uuids.timeBased(); + // create rule chain on edge UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder(); - ruleChainMetadataRequestMsgBuilder.setRuleChainIdMSB(edgeRootRuleChainId.getId().getMostSignificantBits()); - ruleChainMetadataRequestMsgBuilder.setRuleChainIdLSB(edgeRootRuleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); - uplinkMsgBuilder.addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); + RuleChainUpdateMsg.Builder ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder(); + ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + ruleChainUpdateMsgBuilder.setName("Rule Chain Edge"); + ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder); + uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build()); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), edgeRootRuleChainId.getId().getMostSignificantBits()); - Assert.assertEquals(ruleChainMetadataUpdateMsg.getRuleChainIdLSB(), edgeRootRuleChainId.getId().getLeastSignificantBits()); + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataUpdateMsg); - } + RuleChain ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class); + Assert.assertNotNull(ruleChain); + Assert.assertEquals("Rule Chain Edge", ruleChain.getName()); - private void testRuleChainMetadataRequestMsg(RuleChainId ruleChainId) throws Exception { - RuleChainMetadataRequestMsg.Builder ruleChainMetadataRequestMsgBuilder = RuleChainMetadataRequestMsg.newBuilder() - .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()) - .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(ruleChainMetadataRequestMsgBuilder); + // update rule chain on edge + uplinkMsgBuilder = UplinkMsg.newBuilder(); + ruleChainUpdateMsgBuilder = RuleChainUpdateMsg.newBuilder(); + ruleChainUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + ruleChainUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + ruleChainUpdateMsgBuilder.setName("Rule Chain Edge Updated"); + ruleChainUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsgBuilder); + uplinkMsgBuilder.addRuleChainUpdateMsg(ruleChainUpdateMsgBuilder.build()); - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() - .addRuleChainMetadataRequestMsg(ruleChainMetadataRequestMsgBuilder.build()); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof RuleChainMetadataUpdateMsg); - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = (RuleChainMetadataUpdateMsg) latestMessage; - RuleChainId receivedRuleChainId = - new RuleChainId(new UUID(ruleChainMetadataUpdateMsg.getRuleChainIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB())); - Assert.assertEquals(ruleChainId, receivedRuleChainId); + latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + ruleChain = doGet("/api/ruleChain/" + uuid, RuleChain.class); + Assert.assertNotNull(ruleChain); + Assert.assertEquals("Rule Chain Edge Updated", ruleChain.getName()); } private void createRuleChainMetadata(RuleChain ruleChain) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 95cdc0e02d..94e0979ef5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -44,6 +44,8 @@ public interface RuleChainService extends EntityDaoService { RuleChain saveRuleChain(RuleChain ruleChain); + RuleChain saveRuleChain(RuleChain ruleChain, boolean doValidate); + boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index d9da289001..f7d5b5d771 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -420,6 +420,7 @@ message TenantProfileUpdateMsg { bytes profileDataBytes = 8; } +// deprecated message RuleChainMetadataRequestMsg { int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; @@ -557,7 +558,7 @@ message UplinkMsg { repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; repeated AlarmUpdateMsg alarmUpdateMsg = 5; repeated RelationUpdateMsg relationUpdateMsg = 6; - repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; // deprecated repeated AttributesRequestMsg attributesRequestMsg = 8; repeated RelationRequestMsg relationRequestMsg = 9; repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; @@ -571,6 +572,8 @@ message UplinkMsg { repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 19; repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 20; + repeated RuleChainUpdateMsg ruleChainUpdateMsg = 21; + repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 22; } message UplinkResponseMsg { 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 5e6b1e8009..387fc00c6c 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 @@ -108,7 +108,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { - ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + return doSaveRuleChain(ruleChain, true); + } + + @Override + @Transactional + public RuleChain saveRuleChain(RuleChain ruleChain, boolean doValidate) { + return doSaveRuleChain(ruleChain, doValidate); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean doValidate) { + log.trace("Executing doSaveRuleChain [{}]", ruleChain); + if (doValidate) { + ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); + } try { RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); if (ruleChain.getId() == null) { From 2ef441d115fe861dc5ba8ea31ccd6c9780811375 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 15:27:49 +0300 Subject: [PATCH 02/42] Tmp check test logic for rule chain --- .../thingsboard/server/dao/rule/BaseRuleChainService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 387fc00c6c..7e9ae12035 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 @@ -178,11 +178,15 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (nodes != null) { for (RuleNode node : nodes) { setSingletonMode(node); + /* TODO: voba - merge comment if (node.getId() != null) { ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); } else { toAddOrUpdate.add(node); } + */ + ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); + toAddOrUpdate.add(node); } } @@ -193,8 +197,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { - newRuleNode = ruleChainMetaData.getNodes().get(index); - toAddOrUpdate.add(newRuleNode); +// newRuleNode = ruleChainMetaData.getNodes().get(index); +// toAddOrUpdate.add(newRuleNode); } else { updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, null)); toDelete.add(existingNode); From 055fab9ad54a789ecb6451692e6a57c85b2e02bd Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 8 Sep 2023 17:09:06 +0300 Subject: [PATCH 03/42] Fix test: missing bean and improve safe for rule chain --- .../edge/rpc/processor/rule/BaseRuleChainProcessor.java | 4 ++-- .../edge/rpc/processor/rule/RuleChainEdgeProcessor.java | 3 ++- .../service/edge/rpc/processor/BaseEdgeProcessorTest.java | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index dc9fa6e14d..66a471e3a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -41,7 +41,7 @@ import java.util.function.Function; @Slf4j public class BaseRuleChainProcessor extends BaseEdgeProcessor { - protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg) { + protected boolean saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) { boolean created = false; RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, ruleChainId); if (ruleChain == null) { @@ -51,7 +51,7 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { ruleChain.setCreatedTime(Uuids.unixTimestamp(ruleChainId.getId())); } ruleChain.setName(ruleChainUpdateMsg.getName()); - ruleChain.setType(RuleChainType.EDGE); + ruleChain.setType(ruleChainType); ruleChain.setDebugMode(ruleChainUpdateMsg.getDebugMode()); ruleChain.setConfiguration(JacksonUtil.toJsonNode(ruleChainUpdateMsg.getConfiguration())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index ae5e158ba7..e2ce1fe651 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; @@ -105,7 +106,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { - boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg); + boolean created = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); if (created) { createRelationFromEdge(tenantId, edge.getId(), ruleChainId); pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java index 1c4b481302..9519362305 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -196,6 +197,9 @@ public abstract class BaseEdgeProcessorTest { @MockBean protected DataValidator entityViewValidator; + @MockBean + protected DataValidator ruleChainValidator; + @MockBean protected EdgeMsgConstructor edgeMsgConstructor; From 3908105ac5e4cba45100a2020beb61db1a1e34c2 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 13 Jan 2025 13:23:41 +0200 Subject: [PATCH 04/42] fix_bug: tbel, in the isDecimal method. The mantissa format includes numbers written in exponential form --- .../java/org/thingsboard/script/api/tbel/TbUtils.java | 2 +- .../org/thingsboard/script/api/tbel/TbUtilsTest.java | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index fae91d14ca..ab1aef979a 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -1299,7 +1299,7 @@ public class TbUtils { if (str == null || str.isEmpty()) { return -1; } - return str.matches("[+-]?\\d+(\\.\\d+)?") ? DEC_RADIX : -1; + return str.matches("[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+)?") ? DEC_RADIX : -1; } public static int isHexadecimal(String str) { diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 21ee8538a0..4ac387b258 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -442,8 +442,9 @@ public class TbUtilsTest { @Test public void parsDouble() { - String doubleValStr = "1729.1729"; - Assertions.assertEquals(java.util.Optional.of(doubleVal).get(), TbUtils.parseDouble(doubleValStr)); + String doubleValStr = "1.1428250947E8"; + Assertions.assertEquals(Double.parseDouble(doubleValStr), TbUtils.parseDouble(doubleValStr)); + doubleValStr = "1729.1729"; Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseHexToDouble(longValHex))); Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseHexToDouble(longValHex, false))); Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBigEndianHexToDouble(longValHex))); @@ -930,7 +931,13 @@ public class TbUtilsTest { @Test public void isDecimal_Test() { Assertions.assertEquals(10, TbUtils.isDecimal("4567039")); + Assertions.assertEquals(10, TbUtils.isDecimal("1.1428250947E8")); + Assertions.assertEquals(10, TbUtils.isDecimal("123.45")); + Assertions.assertEquals(10, TbUtils.isDecimal("-1.23E-4")); + Assertions.assertEquals(10, TbUtils.isDecimal("1E5")); Assertions.assertEquals(-1, TbUtils.isDecimal("C100110")); + Assertions.assertEquals(-1, TbUtils.isDecimal("abc")); + Assertions.assertEquals(-1, TbUtils.isDecimal(null)); } @Test From e3a72fec09ca0f82584123b2ee2050ec41824db2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 4 Feb 2025 12:39:06 +0200 Subject: [PATCH 05/42] Refactor TopicService. Change ymls to be able to configure notification topic names --- .../src/main/resources/thingsboard.yml | 7 +++- .../server/queue/discovery/TopicService.java | 41 +++++++++++++------ .../src/main/resources/tb-vc-executor.yml | 2 + .../src/main/resources/tb-coap-transport.yml | 4 ++ .../src/main/resources/tb-http-transport.yml | 4 ++ .../src/main/resources/tb-lwm2m-transport.yml | 4 ++ .../src/main/resources/tb-mqtt-transport.yml | 4 ++ .../src/main/resources/tb-snmp-transport.yml | 4 ++ 8 files changed, 57 insertions(+), 13 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f29cc55197..4ee08c7095 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1644,6 +1644,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -1688,7 +1690,6 @@ queue: enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" # Statistics printing interval for Housekeeper print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" - vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" @@ -1720,6 +1721,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine @@ -1743,6 +1746,8 @@ queue: edge: # Default topic name topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" # Amount of partitions used by Edge services partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" # Poll interval for topics related to Edge services diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 927c311a2d..254d0a95d6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -32,6 +32,18 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; + @Value("${queue.core.notifications-topic:tb_core.notifications}") + private String tbCoreNotificationsTopic; + + @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") + private String tbRuleEngineNotificationsTopic; + + @Value("${queue.transport.notifications-topics:tb_transport.notifications}") + private String tbTransportNotificationsTopic; + + @Value("${queue.edge.notifications-topic:tb_edge.notifications}") + private String tbEdgeNotificationsTopic; + private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbRuleEngineNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbEdgeNotificationTopics = new ConcurrentHashMap<>(); @@ -47,19 +59,28 @@ public class TopicService { public TopicPartitionInfo getNotificationsTopic(ServiceType serviceType, String serviceId) { return switch (serviceType) { case TB_CORE -> tbCoreNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); + id -> buildNotificationsTopicPartitionInfo(tbCoreNotificationsTopic, serviceId)); case TB_RULE_ENGINE -> tbRuleEngineNotificationTopics.computeIfAbsent(serviceId, - id -> buildNotificationsTopicPartitionInfo(serviceType, serviceId)); - default -> buildNotificationsTopicPartitionInfo(serviceType, serviceId); + id -> buildNotificationsTopicPartitionInfo(tbRuleEngineNotificationsTopic, serviceId)); + case TB_TRANSPORT -> buildNotificationsTopicPartitionInfo(tbTransportNotificationsTopic, serviceId); + default -> throw new IllegalStateException("Unexpected service type: " + serviceType); }; } + private TopicPartitionInfo buildNotificationsTopicPartitionInfo(String topic, String serviceId) { + return buildTopicPartitionInfo(buildNotificationTopicName(topic, serviceId), null, null, false); + } + + public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { + return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition); + } + public TopicPartitionInfo getEdgeNotificationsTopic(String serviceId) { return tbEdgeNotificationTopics.computeIfAbsent(serviceId, id -> buildEdgeNotificationsTopicPartitionInfo(serviceId)); } private TopicPartitionInfo buildEdgeNotificationsTopicPartitionInfo(String serviceId) { - return buildTopicPartitionInfo("tb_edge.notifications." + serviceId, null, null, false); + return buildTopicPartitionInfo(buildNotificationTopicName(tbEdgeNotificationsTopic, serviceId), null, null, false); } public TopicPartitionInfo getEdgeEventNotificationsTopic(TenantId tenantId, EdgeId edgeId) { @@ -70,18 +91,14 @@ public class TopicService { return buildTopicPartitionInfo("tb_edge_event.notifications." + tenantId + "." + edgeId, null, null, false); } - private TopicPartitionInfo buildNotificationsTopicPartitionInfo(ServiceType serviceType, String serviceId) { - return buildTopicPartitionInfo(serviceType.name().toLowerCase() + ".notifications." + serviceId, null, null, false); - } - - public TopicPartitionInfo buildTopicPartitionInfo(String topic, TenantId tenantId, Integer partition, boolean myPartition) { - return new TopicPartitionInfo(buildTopicName(topic), tenantId, partition, myPartition); - } - public String buildTopicName(String topic) { return prefix.isBlank() ? topic : prefix + "." + topic; } + private String buildNotificationTopicName(String topic, String serviceId) { + return topic + "." + serviceId; + } + public String buildConsumerGroupId(String servicePrefix, TenantId tenantId, String queueName, Integer partitionId) { return this.buildTopicName( servicePrefix + queueName diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index cba83e5ff7..595e514907 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -151,6 +151,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 66ab703a54..794eaed047 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -351,6 +351,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -381,6 +383,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index be91e4fe67..860059b1ee 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -296,6 +296,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -326,6 +328,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 7bdd68baf1..824d998836 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -397,6 +397,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -427,6 +429,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 8d9a60a319..4b0adab7d1 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -330,6 +330,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -360,6 +362,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 4c11bd0018..e6fe253e79 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -283,6 +283,8 @@ queue: core: # Default topic name topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_CORE_NOTIFICATIONS_TOPIC:tb_core.notifications}" # Interval in milliseconds to poll messages by Core microservices poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" # Amount of partitions used by Core microservices @@ -313,6 +315,8 @@ queue: rule-engine: # Deprecated. It will be removed in the nearest releases topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + # For high-priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_RULE_ENGINE_NOTIFICATIONS_TOPIC:tb_rule_engine.notifications}" # Interval in milliseconds to poll messages by Rule Engine poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" # Timeout for processing a message pack of Rule Engine From 8de861f8977d1da48b85959dd520983aaa6a0876 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 7 Feb 2025 16:11:33 +0200 Subject: [PATCH 06/42] Improve customization for edge-event topic --- application/src/main/resources/thingsboard.yml | 2 ++ .../org/thingsboard/server/queue/discovery/TopicService.java | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ee08c7095..7b1df1d9d7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1748,6 +1748,8 @@ queue: topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" # For high-priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" + # For edge events messages + event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}" # Amount of partitions used by Edge services partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" # Poll interval for topics related to Edge services diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 254d0a95d6..bab55eece8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -44,6 +44,9 @@ public class TopicService { @Value("${queue.edge.notifications-topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; + @Value("${queue.edge.event-notifications-topic:tb_edge.notifications}") + private String tbEdgeEventNotificationsTopic; + private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbRuleEngineNotificationTopics = new ConcurrentHashMap<>(); private final ConcurrentMap tbEdgeNotificationTopics = new ConcurrentHashMap<>(); @@ -88,7 +91,7 @@ public class TopicService { } public TopicPartitionInfo buildEdgeEventNotificationsTopicPartitionInfo(TenantId tenantId, EdgeId edgeId) { - return buildTopicPartitionInfo("tb_edge_event.notifications." + tenantId + "." + edgeId, null, null, false); + return buildTopicPartitionInfo(tbEdgeEventNotificationsTopic + "." + tenantId + "." + edgeId, null, null, false); } public String buildTopicName(String topic) { From b54e0cd59c0d70181c509279a80bf985e61fd719 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 7 Feb 2025 16:12:53 +0200 Subject: [PATCH 07/42] Rename tbEdgeEventNotificationsTopic --- .../org/thingsboard/server/queue/discovery/TopicService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index bab55eece8..ca080f9481 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -44,7 +44,7 @@ public class TopicService { @Value("${queue.edge.notifications-topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; - @Value("${queue.edge.event-notifications-topic:tb_edge.notifications}") + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From ec8e7a07268694685cfb047f1fc7724a36e50b2d Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 10 Feb 2025 14:28:46 +0200 Subject: [PATCH 08/42] Added versioning for ApiUsageState entity --- .../src/main/data/upgrade/basic/schema_update.sql | 2 ++ .../thingsboard/server/common/data/ApiUsageState.java | 5 ++++- .../server/dao/model/sql/ApiUsageStateEntity.java | 10 ++++------ dao/src/main/resources/sql/schema-entities.sql | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 5d66056701..29b541327a 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -61,3 +61,5 @@ DO $$ $$; -- UPDATE SAVE TIME SERIES NODES END + +ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java index b13a9edd74..f6f3d82c09 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) @Getter @Setter -public class ApiUsageState extends BaseData implements HasTenantId { +public class ApiUsageState extends BaseData implements HasTenantId, HasVersion { private static final long serialVersionUID = 8250339805336035966L; @@ -41,6 +41,7 @@ public class ApiUsageState extends BaseData implements HasTenan private ApiUsageStateValue emailExecState; private ApiUsageStateValue smsExecState; private ApiUsageStateValue alarmExecState; + private Long version; public ApiUsageState() { super(); @@ -62,6 +63,7 @@ public class ApiUsageState extends BaseData implements HasTenan this.emailExecState = ur.getEmailExecState(); this.smsExecState = ur.getSmsExecState(); this.alarmExecState = ur.getAlarmExecState(); + this.version = ur.getVersion(); } public boolean isTransportEnabled() { @@ -95,4 +97,5 @@ public class ApiUsageState extends BaseData implements HasTenan public boolean isAlarmCreationEnabled() { return alarmExecState != ApiUsageStateValue.DISABLED; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java index 43b99dd7ac..2a65029742 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; import java.util.UUID; @@ -40,7 +40,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.API_USAGE_STATE_TABLE_NAME) -public class ApiUsageStateEntity extends BaseSqlEntity implements BaseEntity { +public class ApiUsageStateEntity extends BaseVersionedEntity implements BaseEntity { @Column(name = ModelConstants.API_USAGE_STATE_TENANT_ID_COLUMN) private UUID tenantId; @@ -77,10 +77,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements } public ApiUsageStateEntity(ApiUsageState ur) { - if (ur.getId() != null) { - this.setUuid(ur.getId().getId()); - } - this.setCreatedTime(ur.getCreatedTime()); + super(ur); if (ur.getTenantId() != null) { this.tenantId = ur.getTenantId().getId(); } @@ -116,6 +113,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements ur.setEmailExecState(emailExecState); ur.setSmsExecState(smsExecState); ur.setAlarmExecState(alarmExecState); + ur.setVersion(version); return ur; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 916a5487f8..71afc03081 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -702,6 +702,7 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( email_exec varchar(32), sms_exec varchar(32), alarm_exec varchar(32), + version BIGINT DEFAULT 1, CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) ); From 61f77f301d2a08d2448b4d8517cbfb720ace32e7 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Fri, 14 Feb 2025 19:21:03 +0200 Subject: [PATCH 09/42] Resolved review comments --- .../service/apiusage/BaseApiUsageState.java | 3 +- .../DefaultTbApiUsageStateService.java | 3 +- .../DefaultTbApiUsageStateServiceTest.java | 142 ++++++++++++++++++ .../dao/usagerecord/ApiUsageStateService.java | 1 + .../server/common/data/ApiUsageState.java | 4 +- .../server/common/util/ProtoUtils.java | 5 +- common/proto/src/main/proto/queue.proto | 1 + .../usagerecord/ApiUsageStateServiceImpl.java | 1 + .../dao/service/ApiUsageStateServiceTest.java | 47 ++++-- 9 files changed, 189 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index 9eac721dfb..9db4949d5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -40,7 +40,8 @@ public abstract class BaseApiUsageState { private final Map gaugesReportCycles = new HashMap<>(); @Getter - private final ApiUsageState apiUsageState; + @Setter + private ApiUsageState apiUsageState; @Getter private volatile long currentCycleTs; @Getter diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 3a351528e4..30a3af7f65 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -355,7 +355,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService private void persistAndNotify(BaseApiUsageState state, Map result) { log.info("[{}] Detected update of the API state for {}: {}", state.getEntityId(), state.getEntityType(), result); - apiUsageStateService.update(state.getApiUsageState()); + ApiUsageState updatedState = apiUsageStateService.update(state.getApiUsageState()); + state.setApiUsageState(updatedState); long ts = System.currentTimeMillis(); List stateTelemetry = new ArrayList<>(); result.forEach((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))); diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 051214c771..a0f61a4928 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.apiusage; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,14 +24,43 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.ApiFeature; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.telemetry.InternalTelemetryService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -38,6 +68,21 @@ public class DefaultTbApiUsageStateServiceTest { @Mock TenantApiUsageState tenantUsageStateMock; + @Mock + private NotificationRuleProcessor notificationRuleProcessor; + + @Mock + private ApiUsageStateService apiUsageStateService; + + @Mock + private TenantService tenantService; + + @Mock + private InternalTelemetryService tsWsService; + + @Mock + private MailExecutorService mailExecutor; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); @Spy @@ -46,6 +91,7 @@ public class DefaultTbApiUsageStateServiceTest { @BeforeEach public void setUp() { + ReflectionTestUtils.setField(service, "tsWsService", tsWsService); } @Test @@ -56,4 +102,100 @@ public class DefaultTbApiUsageStateServiceTest { Mockito.verify(service, never()).getOrFetchState(tenantId, tenantId); } + @Test + public void testAllApiFeaturesDisabledWhenLimitReached() { + doReturn(null).when(tsWsService).saveTimeseriesInternal(any()); + + TenantApiUsageState tenantUsageStateMock = mock(TenantApiUsageState.class); + ApiUsageState apiUsageState = getApiUsageState(); + when(tenantUsageStateMock.getApiUsageState()).thenReturn(apiUsageState); + + doReturn(BaseApiUsageState.StatsCalculationResult.builder() + .newValue(200L) + .valueChanged(true) + .newHourlyValue(200L) + .hourlyValueChanged(true) + .build()) + .when(tenantUsageStateMock).calculate(any(ApiUsageRecordKey.class), anyLong(), anyString()); + + doReturn(200L).when(tenantUsageStateMock).getProfileThreshold(any(ApiUsageRecordKey.class)); + doReturn(50L).when(tenantUsageStateMock).getProfileWarnThreshold(any(ApiUsageRecordKey.class)); + doReturn(300L).when(tenantUsageStateMock).get(any(ApiUsageRecordKey.class)); + + when(tenantUsageStateMock.getEntityType()).thenReturn(EntityType.TENANT); + when(tenantUsageStateMock.getEntityId()).thenReturn(tenantId); + + Map expectedResult = getExpectedResult(); + when(tenantUsageStateMock.checkStateUpdatedDueToThreshold(any())).thenReturn(expectedResult); + service.myUsageStates.put(tenantId, tenantUsageStateMock); + + when(apiUsageStateService.update(apiUsageState)).thenReturn(apiUsageState); + + Tenant dummyTenant = new Tenant(); + dummyTenant.setEmail("test@example.com"); + when(tenantService.findTenantById(any())).thenReturn(dummyTenant); + + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder(); + UUID uuid = tenantId.getId(); + msgBuilder.setTenantIdMSB(uuid.getMostSignificantBits()) + .setTenantIdLSB(uuid.getLeastSignificantBits()); + msgBuilder.setCustomerIdMSB(0) + .setCustomerIdLSB(0); + msgBuilder.setServiceId("TEST_SERVICE"); + + List usageStats = new ArrayList<>(); + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { + if (recordKey.getApiFeature() != null) { + TransportProtos.UsageStatsKVProto stat = TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(recordKey.name()) + .setValue(1000L) + .build(); + usageStats.add(stat); + msgBuilder.addValues(stat); + } + } + + TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); + TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbCallback callback = mock(TbCallback.class); + + service.process(queueMsg, callback); + verify(callback).onSuccess(); + + for (ApiFeature feature : expectedResult.keySet()) { + verify(notificationRuleProcessor, atLeastOnce()).process(argThat(trigger -> + trigger instanceof ApiUsageLimitTrigger && + ((ApiUsageLimitTrigger) trigger).getStatus() == ApiUsageStateValue.DISABLED && + ((ApiUsageLimitTrigger) trigger).getState().getApiFeature().getApiStateKey().equals(feature.getApiStateKey()) + )); + } + } + + + @NotNull + private ApiUsageState getApiUsageState() { + ApiUsageState apiUsageState = new ApiUsageState(); + + apiUsageState.setTenantId(tenantId); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + return apiUsageState; + } + + private Map getExpectedResult() { + Map expectedResult = new HashMap<>(); + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { + if (recordKey.getApiFeature() != null) { + expectedResult.put(recordKey.getApiFeature(), ApiUsageStateValue.DISABLED); + } + } + + return expectedResult; + } } \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java index 3d09247c7e..9512b72ecd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java @@ -34,4 +34,5 @@ public interface ApiUsageStateService extends EntityDaoService { void deleteApiUsageStateByEntityId(EntityId entityId); ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java index f6f3d82c09..9b2a2b0046 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -86,11 +86,11 @@ public class ApiUsageState extends BaseData implements HasTenan return !ApiUsageStateValue.DISABLED.equals(tbelExecState); } - public boolean isEmailSendEnabled(){ + public boolean isEmailSendEnabled() { return !ApiUsageStateValue.DISABLED.equals(emailExecState); } - public boolean isSmsSendEnabled(){ + public boolean isSmsSendEnabled() { return !ApiUsageStateValue.DISABLED.equals(smsExecState); } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 264b23f118..dd376fe746 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -1019,7 +1019,9 @@ public class ProtoUtils { .setTbelExecState(apiUsageState.getTbelExecState().name()) .setEmailExecState(apiUsageState.getEmailExecState().name()) .setSmsExecState(apiUsageState.getSmsExecState().name()) - .setAlarmExecState(apiUsageState.getAlarmExecState().name()).build(); + .setAlarmExecState(apiUsageState.getAlarmExecState().name()) + .setVersion(apiUsageState.getVersion()) + .build(); } public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) { @@ -1035,6 +1037,7 @@ public class ProtoUtils { apiUsageState.setEmailExecState(ApiUsageStateValue.valueOf(proto.getEmailExecState())); apiUsageState.setSmsExecState(ApiUsageStateValue.valueOf(proto.getSmsExecState())); apiUsageState.setAlarmExecState(ApiUsageStateValue.valueOf(proto.getAlarmExecState())); + apiUsageState.setVersion(proto.getVersion()); return apiUsageState; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 228a4039d2..25c1f5dd01 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -325,6 +325,7 @@ message ApiUsageStateProto { string emailExecState = 14; string smsExecState = 15; string alarmExecState = 16; + int64 version = 17; } message RepositorySettingsProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index d901ae950e..3d0e0f1309 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -154,6 +154,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A log.trace("Executing save [{}]", apiUsageState.getTenantId()); validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id); validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); + apiUsageState.setVersion(null); ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId()) .entity(savedState).build()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java index 36b138f42a..d3e86a1603 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; @@ -30,22 +31,42 @@ public class ApiUsageStateServiceTest extends AbstractServiceTest { ApiUsageStateService apiUsageStateService; @Test - public void testFindApiUsageStateByTenantId() { - ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); + public void testFindTenantApiUsageState() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(state); } @Test - public void testUpdateApiUsageState(){ - ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); - Assert.assertTrue(apiUsageState.isTransportEnabled()); - apiUsageState.setTransportState(ApiUsageStateValue.DISABLED); - apiUsageState = apiUsageStateService.update(apiUsageState); - Assert.assertNotNull(apiUsageState); - apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); - Assert.assertFalse(apiUsageState.isTransportEnabled()); + public void testUpdate() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + + state.setTransportState(ApiUsageStateValue.DISABLED); + ApiUsageState updated = apiUsageStateService.update(state); + Assert.assertEquals(ApiUsageStateValue.DISABLED, updated.getTransportState()); + } + + @Test + public void testUpdateWithNullId() { + ApiUsageState newState = new ApiUsageState(); + newState.setTenantId(tenantId); + newState.setTransportState(ApiUsageStateValue.ENABLED); + Assert.assertThrows(IncorrectParameterException.class, () -> apiUsageStateService.update(newState)); + } + + @Test + public void testFindApiUsageStateByEntityId() { + ApiUsageState state = apiUsageStateService.findApiUsageStateByEntityId(tenantId); + Assert.assertNotNull(state); + } + + @Test + public void testDeleteByTenantId() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(state); + + apiUsageStateService.deleteByTenantId(tenantId); + state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNull(state); } } From 9542c65ca3d00f32d161fdd09ce162d1b50ee024 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 15:39:59 +0200 Subject: [PATCH 10/42] Fixed api limit tests for ApiUsage service --- .../DefaultTbApiUsageStateServiceTest.java | 260 +++++++++++------- 1 file changed, 153 insertions(+), 107 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index a0f61a4928..100c98adec 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.service.apiusage; -import org.jetbrains.annotations.NotNull; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,42 +26,28 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.server.common.data.ApiFeature; -import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.ApiUsageState; -import org.thingsboard.server.common.data.ApiUsageStateValue; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.telemetry.InternalTelemetryService; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -80,122 +67,181 @@ public class DefaultTbApiUsageStateServiceTest { @Mock private InternalTelemetryService tsWsService; + @Mock + private PartitionService partitionService; + @Mock private MailExecutorService mailExecutor; - - TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); @Spy @InjectMocks DefaultTbApiUsageStateService service; + private ApiUsageState dummyUsageState; + private TenantApiUsageState tenantApiUsageState; + private Tenant dummyTenant; + + private static final int MAX_ENABLE_VALUE = 5000; + private static final double WARN_THRESHOLD_VALUE = 0.8; + @BeforeEach public void setUp() { + var tenantId = TenantId.fromUUID(UUID.randomUUID()); + dummyTenant = new Tenant(); + dummyTenant.setId(tenantId); + dummyTenant.setEmail("test@tenant.com"); + + lenient().when(tsWsService.saveTimeseriesInternal(any())).thenReturn(Futures.immediateFuture(0)); + ReflectionTestUtils.setField(service, "tsWsService", tsWsService); + + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(MAX_ENABLE_VALUE) + .maxDPStorageDays(MAX_ENABLE_VALUE) + .maxREExecutions(MAX_ENABLE_VALUE) + .maxEmails(MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) + .build(); + + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(config); + + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setId(new TenantProfileId(UUID.randomUUID())); + tenantProfile.setProfileData(profileData); + + dummyUsageState = new ApiUsageState(); + dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); + dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setTenantId(tenantId); + dummyUsageState.setEntityId(tenantId); + dummyUsageState.setVersion(1L); + + tenantApiUsageState = new TenantApiUsageState(tenantProfile, dummyUsageState); + + service.myUsageStates.put(tenantId, tenantApiUsageState); + } + + @AfterEach + public void tearDown() { + } @Test public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - service.myUsageStates.put(tenantId, tenantUsageStateMock); - ApiUsageState tenantUsageState = service.getApiUsageState(tenantId); + service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); + ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - Mockito.verify(service, never()).getOrFetchState(tenantId, tenantId); + Mockito.verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } @Test - public void testAllApiFeaturesDisabledWhenLimitReached() { - doReturn(null).when(tsWsService).saveTimeseriesInternal(any()); - - TenantApiUsageState tenantUsageStateMock = mock(TenantApiUsageState.class); - ApiUsageState apiUsageState = getApiUsageState(); - when(tenantUsageStateMock.getApiUsageState()).thenReturn(apiUsageState); - - doReturn(BaseApiUsageState.StatsCalculationResult.builder() - .newValue(200L) - .valueChanged(true) - .newHourlyValue(200L) - .hourlyValueChanged(true) - .build()) - .when(tenantUsageStateMock).calculate(any(ApiUsageRecordKey.class), anyLong(), anyString()); - - doReturn(200L).when(tenantUsageStateMock).getProfileThreshold(any(ApiUsageRecordKey.class)); - doReturn(50L).when(tenantUsageStateMock).getProfileWarnThreshold(any(ApiUsageRecordKey.class)); - doReturn(300L).when(tenantUsageStateMock).get(any(ApiUsageRecordKey.class)); - - when(tenantUsageStateMock.getEntityType()).thenReturn(EntityType.TENANT); - when(tenantUsageStateMock.getEntityId()).thenReturn(tenantId); - - Map expectedResult = getExpectedResult(); - when(tenantUsageStateMock.checkStateUpdatedDueToThreshold(any())).thenReturn(expectedResult); - service.myUsageStates.put(tenantId, tenantUsageStateMock); - - when(apiUsageStateService.update(apiUsageState)).thenReturn(apiUsageState); - - Tenant dummyTenant = new Tenant(); - dummyTenant.setEmail("test@example.com"); - when(tenantService.findTenantById(any())).thenReturn(dummyTenant); - - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder(); - UUID uuid = tenantId.getId(); - msgBuilder.setTenantIdMSB(uuid.getMostSignificantBits()) - .setTenantIdLSB(uuid.getLeastSignificantBits()); - msgBuilder.setCustomerIdMSB(0) - .setCustomerIdLSB(0); - msgBuilder.setServiceId("TEST_SERVICE"); - - List usageStats = new ArrayList<>(); - for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { - if (recordKey.getApiFeature() != null) { - TransportProtos.UsageStatsKVProto stat = TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(recordKey.name()) - .setValue(1000L) - .build(); - usageStats.add(stat); - msgBuilder.addValues(stat); + public void testProcess_AllFeaturesTransitionToWarning() { + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) + .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(0) + .setCustomerIdLSB(0) + .setServiceId("testService"); + + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null) { + msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(key.name()) + .setValue(4500L) + .build()); } } TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); TbCallback callback = mock(TbCallback.class); - service.process(queueMsg, callback); - verify(callback).onSuccess(); + when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { + ApiUsageState state = invocation.getArgument(0); + state.setVersion(2L); + return state; + }); + + when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - for (ApiFeature feature : expectedResult.keySet()) { - verify(notificationRuleProcessor, atLeastOnce()).process(argThat(trigger -> - trigger instanceof ApiUsageLimitTrigger && - ((ApiUsageLimitTrigger) trigger).getStatus() == ApiUsageStateValue.DISABLED && - ((ApiUsageLimitTrigger) trigger).getState().getApiFeature().getApiStateKey().equals(feature.getApiStateKey()) - )); + service.process(msg, callback); + + verify(callback, times(1)).onSuccess(); + + for (ApiFeature feature : ApiFeature.values()) { + if (containsFeature(feature)) { + assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(feature), + "For feature " + feature + " expected state WARNING"); + } } - } + + assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); - @NotNull - private ApiUsageState getApiUsageState() { - ApiUsageState apiUsageState = new ApiUsageState(); - - apiUsageState.setTenantId(tenantId); - apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); - apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); - apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); - return apiUsageState; + verify(notificationRuleProcessor, atLeastOnce()).process(any()); + verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } - private Map getExpectedResult() { - Map expectedResult = new HashMap<>(); - for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { - if (recordKey.getApiFeature() != null) { - expectedResult.put(recordKey.getApiFeature(), ApiUsageStateValue.DISABLED); + @Test + public void testProcess_AllFeaturesTransitionToDisabled() { + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) + .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(0) + .setCustomerIdLSB(0) + .setServiceId("testService"); + + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null) { + msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(key.name()) + .setValue(5500L) + .build()); + } + } + TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbCallback callback = mock(TbCallback.class); + + when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { + ApiUsageState state = invocation.getArgument(0); + state.setVersion(2L); + return state; + }); + when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); + + service.process(msg, callback); + verify(callback, times(1)).onSuccess(); + + for (ApiFeature feature : ApiFeature.values()) { + if (containsFeature(feature)) { + assertEquals(ApiUsageStateValue.DISABLED, tenantApiUsageState.getFeatureValue(feature), + "For feature " + feature + " expected state DISABLED"); } } + verify(notificationRuleProcessor, atLeastOnce()).process(any()); + verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); + } - return expectedResult; + private boolean containsFeature(ApiFeature feature) { + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { + return true; + } + } + return false; } + } \ No newline at end of file From 9792c28545d5070431a6b77fcec4415e4bf1171e Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 16:13:09 +0200 Subject: [PATCH 11/42] Refactored import format --- .../apiusage/DefaultTbApiUsageStateServiceTest.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 100c98adec..c9abb8e1c0 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -46,8 +46,14 @@ import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -143,7 +149,7 @@ public class DefaultTbApiUsageStateServiceTest { service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - Mockito.verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); + verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } @Test From 5ced8457f16241b71894d31b5a471d25159b37bf Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 20:53:49 +0200 Subject: [PATCH 12/42] Added new test cases --- .../DefaultTbApiUsageStateServiceTest.java | 168 +++++++++++++++--- 1 file changed, 144 insertions(+), 24 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index c9abb8e1c0..8fefde0f6c 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -16,13 +16,12 @@ package org.thingsboard.server.service.apiusage; import com.google.common.util.concurrent.Futures; -import org.junit.jupiter.api.AfterEach; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -33,6 +32,8 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -46,8 +47,11 @@ import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -58,9 +62,6 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { - @Mock - TenantApiUsageState tenantUsageStateMock; - @Mock private NotificationRuleProcessor notificationRuleProcessor; @@ -73,21 +74,27 @@ public class DefaultTbApiUsageStateServiceTest { @Mock private InternalTelemetryService tsWsService; + @Mock + private MailExecutorService mailExecutor; + @Mock private PartitionService partitionService; @Mock - private MailExecutorService mailExecutor; + private TbTenantProfileCache tbTenantProfileCache; @Spy @InjectMocks DefaultTbApiUsageStateService service; - private ApiUsageState dummyUsageState; private TenantApiUsageState tenantApiUsageState; private Tenant dummyTenant; private static final int MAX_ENABLE_VALUE = 5000; + private static final long VALUE_WARNING = 4500L; + private static final long VALUE_DISABLE = 5500L; + private static final int NEW_MAX_ENABLE_VALUE = 4000; + private static final double NEW_WARN_THRESHOLD_VALUE = 0.9; private static final double WARN_THRESHOLD_VALUE = 0.8; @BeforeEach @@ -117,11 +124,20 @@ public class DefaultTbApiUsageStateServiceTest { TenantProfileData profileData = new TenantProfileData(); profileData.setConfiguration(config); - TenantProfile tenantProfile = new TenantProfile(); - tenantProfile.setId(new TenantProfileId(UUID.randomUUID())); - tenantProfile.setProfileData(profileData); + TenantProfile dummyTenantProfile = new TenantProfile(); + dummyTenantProfile.setId(new TenantProfileId(UUID.randomUUID())); + dummyTenantProfile.setProfileData(profileData); + + ApiUsageState dummyUsageState = getApiUsageState(tenantId); + + tenantApiUsageState = new TenantApiUsageState(dummyTenantProfile, dummyUsageState); - dummyUsageState = new ApiUsageState(); + service.myUsageStates.put(tenantId, tenantApiUsageState); + } + + @NotNull + private static ApiUsageState getApiUsageState(TenantId tenantId) { + ApiUsageState dummyUsageState = new ApiUsageState(); dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); @@ -133,23 +149,61 @@ public class DefaultTbApiUsageStateServiceTest { dummyUsageState.setTenantId(tenantId); dummyUsageState.setEntityId(tenantId); dummyUsageState.setVersion(1L); + return dummyUsageState; + } - tenantApiUsageState = new TenantApiUsageState(tenantProfile, dummyUsageState); - - service.myUsageStates.put(tenantId, tenantApiUsageState); + @Test + public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { + ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); + assertThat(tenantUsageState, is(tenantApiUsageState.getApiUsageState())); + verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } - @AfterEach - public void tearDown() { + @Test + public void testGetApiUsageState_fromOtherUsageStates() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + ApiUsageState otherState = new ApiUsageState(); + otherState.setTenantId(tenantId); + service.otherUsageStates.put(tenantId, otherState); + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(otherState)); + } + @Test + public void testGetApiUsageState_whenMyPartition() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + service.otherUsageStates.remove(tenantId); + + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + when(tpi.isMyPartition()).thenReturn(true); + when(partitionService.resolve(any(), eq(dummyTenant.getId()), eq(dummyTenant.getId()))).thenReturn(tpi); + + TenantApiUsageState fetchedState = tenantApiUsageState; + doReturn(fetchedState).when(service).getOrFetchState(tenantId, tenantId); + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(fetchedState.getApiUsageState())); } @Test - public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); - ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); - assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); + public void testGetApiUsageState_whenNotMyPartition() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + service.otherUsageStates.remove(tenantId); + + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + when(tpi.isMyPartition()).thenReturn(false); + when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(tpi); + + ApiUsageState foundState = new ApiUsageState(); + foundState.setTenantId(tenantId); + when(apiUsageStateService.findTenantApiUsageState(tenantId)).thenReturn(foundState); + + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(foundState)); + + assertThat(service.otherUsageStates.get(tenantId), is(foundState)); } @Test @@ -165,7 +219,7 @@ public class DefaultTbApiUsageStateServiceTest { if (key.getApiFeature() != null) { msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() .setKey(key.name()) - .setValue(4500L) + .setValue(VALUE_WARNING) .build()); } } @@ -195,7 +249,6 @@ public class DefaultTbApiUsageStateServiceTest { assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); - verify(notificationRuleProcessor, atLeastOnce()).process(any()); verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } @@ -213,7 +266,7 @@ public class DefaultTbApiUsageStateServiceTest { if (key.getApiFeature() != null) { msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() .setKey(key.name()) - .setValue(5500L) + .setValue(VALUE_DISABLE) .build()); } } @@ -241,6 +294,73 @@ public class DefaultTbApiUsageStateServiceTest { verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } + @Test + public void testOnTenantProfileUpdate_updatesStateForMatchingTenant() { + TenantProfileId currentProfileId = tenantApiUsageState.getTenantProfileId(); + + TenantProfileData newProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(NEW_MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(MAX_ENABLE_VALUE) + .maxDPStorageDays(NEW_MAX_ENABLE_VALUE) + .maxREExecutions(MAX_ENABLE_VALUE) + .maxEmails(NEW_MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .build(); + + newProfileData.setConfiguration(newConfig); + + TenantProfile newProfile = new TenantProfile(); + newProfile.setId(currentProfileId); + newProfile.setProfileData(newProfileData); + + when(tbTenantProfileCache.get(currentProfileId)).thenReturn(newProfile); + + service.onTenantProfileUpdate(currentProfileId); + + assertEquals(currentProfileId, tenantApiUsageState.getTenantProfileId()); + assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); + } + + @Test + public void testOnTenantUpdate_updatesStateWhenProfileChanged() { + TenantProfileId oldProfileId = tenantApiUsageState.getTenantProfileId(); + + TenantProfileId newProfileId = new TenantProfileId(UUID.randomUUID()); + TenantProfileData newProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(NEW_MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(NEW_MAX_ENABLE_VALUE) + .maxDPStorageDays(MAX_ENABLE_VALUE) + .maxREExecutions(NEW_MAX_ENABLE_VALUE) + .maxEmails(MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .build(); + + newProfileData.setConfiguration(newConfig); + + TenantProfile newProfile = new TenantProfile(); + newProfile.setId(newProfileId); + newProfile.setProfileData(newProfileData); + + when(tbTenantProfileCache.get(dummyTenant.getTenantId())).thenReturn(newProfile); + + assertNotEquals(oldProfileId, newProfileId); + + service.onTenantUpdate(dummyTenant.getTenantId()); + + assertEquals(newProfileId, tenantApiUsageState.getTenantProfileId()); + assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); + } + private boolean containsFeature(ApiFeature feature) { for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { From a118c44cf51d0f1daa0a8896e0d5fb490675c5d8 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Wed, 19 Feb 2025 18:26:34 +0200 Subject: [PATCH 13/42] Added tests on api usage --- .../server/service/apiusage/ApiUsageTest.java | 144 +++++++ .../DefaultTbApiUsageStateServiceTest.java | 358 +++--------------- 2 files changed, 199 insertions(+), 303 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java new file mode 100644 index 0000000000..c708525a1d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.apiusage; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.controller.TbUrlConstants; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@TestPropertySource(properties = { + "usage.stats.report.enabled=true", + "usage.stats.report.interval=2", + "usage.stats.gauge_report_interval=1", +}) +public class ApiUsageTest extends AbstractControllerTest { + + private Tenant savedTenant; + private User tenantAdmin; + + private static final int MAX_DP_ENABLE_VALUE = 12; + private static final double WARN_THRESHOLD_VALUE = 0.5; + @Autowired + private ApiUsageStateService apiUsageStateService; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + TenantProfile tenantProfile = createTenantProfile(); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + assertNotNull(savedTenantProfile); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + savedTenant = saveTenant(tenant); + tenantId = savedTenant.getId(); + assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @Test + public void testTelemetryApiCall() throws Exception { + Device device = createDevice(); + assertNotNull(device); + String telemetryPayload = "{\"temperature\":25, \"humidity\":60}"; + String url = TbUrlConstants.TELEMETRY_URL_PREFIX + "/DEVICE/" + device.getId() + "/timeseries/ANY"; + + long VALUE_WARNING = (long) (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE) / 2; + + for (int i = 0; i < VALUE_WARNING; i++) { + doPostAsync(url, telemetryPayload, String.class, status().isOk()); + } + + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); + + long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2; + + for (int i = 0; i < VALUE_DISABLE; i++) { + doPostAsync(url, telemetryPayload, String.class, status().isOk()); + } + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); + }); + } + + private TenantProfile createTenantProfile() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Tenant Profile"); + tenantProfile.setDescription("Tenant Profile" + " Test"); + + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() + .maxDPStorageDays(MAX_DP_ENABLE_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) + .build(); + + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + return tenantProfile; + } + + private Device createDevice() throws Exception { + String testToken = "TEST_TOKEN"; + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + device.setTenantId(tenantId); + + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(testToken); + + SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials); + + return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 8fefde0f6c..8d4c1b849b 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,359 +15,111 @@ */ package org.thingsboard.server.service.apiusage; -import com.google.common.util.concurrent.Futures; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.service.mail.MailExecutorService; -import org.thingsboard.server.service.telemetry.InternalTelemetryService; import java.util.UUID; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.assertEquals; -@ExtendWith(MockitoExtension.class) -public class DefaultTbApiUsageStateServiceTest { +@DaoSqlTest +public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { - @Mock - private NotificationRuleProcessor notificationRuleProcessor; + @Autowired + DefaultTbApiUsageStateService service; - @Mock + @Autowired private ApiUsageStateService apiUsageStateService; - @Mock - private TenantService tenantService; - - @Mock - private InternalTelemetryService tsWsService; - - @Mock - private MailExecutorService mailExecutor; - - @Mock - private PartitionService partitionService; - - @Mock - private TbTenantProfileCache tbTenantProfileCache; - - @Spy - @InjectMocks - DefaultTbApiUsageStateService service; - - private TenantApiUsageState tenantApiUsageState; - private Tenant dummyTenant; + private TenantId tenantId; + private Tenant savedTenant; private static final int MAX_ENABLE_VALUE = 5000; private static final long VALUE_WARNING = 4500L; private static final long VALUE_DISABLE = 5500L; - private static final int NEW_MAX_ENABLE_VALUE = 4000; - private static final double NEW_WARN_THRESHOLD_VALUE = 0.9; private static final double WARN_THRESHOLD_VALUE = 0.8; - @BeforeEach - public void setUp() { - var tenantId = TenantId.fromUUID(UUID.randomUUID()); - dummyTenant = new Tenant(); - dummyTenant.setId(tenantId); - dummyTenant.setEmail("test@tenant.com"); - - lenient().when(tsWsService.saveTimeseriesInternal(any())).thenReturn(Futures.immediateFuture(0)); - - ReflectionTestUtils.setField(service, "tsWsService", tsWsService); - - DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(MAX_ENABLE_VALUE) - .maxDPStorageDays(MAX_ENABLE_VALUE) - .maxREExecutions(MAX_ENABLE_VALUE) - .maxEmails(MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(WARN_THRESHOLD_VALUE) - .build(); - - TenantProfileData profileData = new TenantProfileData(); - profileData.setConfiguration(config); - - TenantProfile dummyTenantProfile = new TenantProfile(); - dummyTenantProfile.setId(new TenantProfileId(UUID.randomUUID())); - dummyTenantProfile.setProfileData(profileData); + @Before + public void init() throws Exception { + loginSysAdmin(); - ApiUsageState dummyUsageState = getApiUsageState(tenantId); + TenantProfile tenantProfile = createTenantProfile(); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + Assert.assertNotNull(savedTenantProfile); - tenantApiUsageState = new TenantApiUsageState(dummyTenantProfile, dummyUsageState); - - service.myUsageStates.put(tenantId, tenantApiUsageState); - } - - @NotNull - private static ApiUsageState getApiUsageState(TenantId tenantId) { - ApiUsageState dummyUsageState = new ApiUsageState(); - dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); - dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); - dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setJsExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setTenantId(tenantId); - dummyUsageState.setEntityId(tenantId); - dummyUsageState.setVersion(1L); - return dummyUsageState; - } - - @Test - public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); - assertThat(tenantUsageState, is(tenantApiUsageState.getApiUsageState())); - verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); - } - - @Test - public void testGetApiUsageState_fromOtherUsageStates() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - ApiUsageState otherState = new ApiUsageState(); - otherState.setTenantId(tenantId); - service.otherUsageStates.put(tenantId, otherState); - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(otherState)); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + savedTenant = saveTenant(tenant); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); } @Test - public void testGetApiUsageState_whenMyPartition() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - service.otherUsageStates.remove(tenantId); - - TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); - when(tpi.isMyPartition()).thenReturn(true); - when(partitionService.resolve(any(), eq(dummyTenant.getId()), eq(dummyTenant.getId()))).thenReturn(tpi); - - TenantApiUsageState fetchedState = tenantApiUsageState; - doReturn(fetchedState).when(service).getOrFetchState(tenantId, tenantId); - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(fetchedState.getApiUsageState())); - } - - @Test - public void testGetApiUsageState_whenNotMyPartition() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - service.otherUsageStates.remove(tenantId); - - TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); - when(tpi.isMyPartition()).thenReturn(false); - when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(tpi); - - ApiUsageState foundState = new ApiUsageState(); - foundState.setTenantId(tenantId); - when(apiUsageStateService.findTenantApiUsageState(tenantId)).thenReturn(foundState); - - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(foundState)); - - assertThat(service.otherUsageStates.get(tenantId), is(foundState)); - } - - @Test - public void testProcess_AllFeaturesTransitionToWarning() { - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() - .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) - .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + public void testProcess_transitionFromWarningToDisabled() { + TransportProtos.ToUsageStatsServiceMsg.Builder warningMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setCustomerIdMSB(0) .setCustomerIdLSB(0) .setServiceId("testService"); - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null) { - msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(key.name()) + warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) .setValue(VALUE_WARNING) .build()); - } - } - - TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); - TbCallback callback = mock(TbCallback.class); - - when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { - ApiUsageState state = invocation.getArgument(0); - state.setVersion(2L); - return state; - }); - - when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - - service.process(msg, callback); - - verify(callback, times(1)).onSuccess(); - for (ApiFeature feature : ApiFeature.values()) { - if (containsFeature(feature)) { - assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(feature), - "For feature " + feature + " expected state WARNING"); - } - } + TransportProtos.ToUsageStatsServiceMsg warningStatsMsg = warningMsgBuilder.build(); + TbProtoQueueMsg warningMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), warningStatsMsg); - assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); + service.process(warningMsg, TbCallback.EMPTY); + assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); - verify(notificationRuleProcessor, atLeastOnce()).process(any()); - verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); - } - - @Test - public void testProcess_AllFeaturesTransitionToDisabled() { - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() - .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) - .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + TransportProtos.ToUsageStatsServiceMsg.Builder disableMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setCustomerIdMSB(0) .setCustomerIdLSB(0) .setServiceId("testService"); - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null) { - msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(key.name()) + disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) .setValue(VALUE_DISABLE) .build()); - } - } - TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); - TbCallback callback = mock(TbCallback.class); - - when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { - ApiUsageState state = invocation.getArgument(0); - state.setVersion(2L); - return state; - }); - when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - service.process(msg, callback); - verify(callback, times(1)).onSuccess(); + TransportProtos.ToUsageStatsServiceMsg disableStatsMsg = disableMsgBuilder.build(); + TbProtoQueueMsg disableMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), disableStatsMsg); - for (ApiFeature feature : ApiFeature.values()) { - if (containsFeature(feature)) { - assertEquals(ApiUsageStateValue.DISABLED, tenantApiUsageState.getFeatureValue(feature), - "For feature " + feature + " expected state DISABLED"); - } - } - verify(notificationRuleProcessor, atLeastOnce()).process(any()); - verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); + service.process(disableMsg, TbCallback.EMPTY); + assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); } - @Test - public void testOnTenantProfileUpdate_updatesStateForMatchingTenant() { - TenantProfileId currentProfileId = tenantApiUsageState.getTenantProfileId(); - - TenantProfileData newProfileData = new TenantProfileData(); - DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(NEW_MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(MAX_ENABLE_VALUE) - .maxDPStorageDays(NEW_MAX_ENABLE_VALUE) - .maxREExecutions(MAX_ENABLE_VALUE) - .maxEmails(NEW_MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(NEW_WARN_THRESHOLD_VALUE) - .build(); - - newProfileData.setConfiguration(newConfig); + private TenantProfile createTenantProfile() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Tenant Profile"); + tenantProfile.setDescription("Tenant Profile" + " Test"); - TenantProfile newProfile = new TenantProfile(); - newProfile.setId(currentProfileId); - newProfile.setProfileData(newProfileData); - - when(tbTenantProfileCache.get(currentProfileId)).thenReturn(newProfile); - - service.onTenantProfileUpdate(currentProfileId); - - assertEquals(currentProfileId, tenantApiUsageState.getTenantProfileId()); - assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); - } - - @Test - public void testOnTenantUpdate_updatesStateWhenProfileChanged() { - TenantProfileId oldProfileId = tenantApiUsageState.getTenantProfileId(); - - TenantProfileId newProfileId = new TenantProfileId(UUID.randomUUID()); - TenantProfileData newProfileData = new TenantProfileData(); - DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(NEW_MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(NEW_MAX_ENABLE_VALUE) + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() .maxDPStorageDays(MAX_ENABLE_VALUE) - .maxREExecutions(NEW_MAX_ENABLE_VALUE) - .maxEmails(MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) .build(); - newProfileData.setConfiguration(newConfig); - - TenantProfile newProfile = new TenantProfile(); - newProfile.setId(newProfileId); - newProfile.setProfileData(newProfileData); - - when(tbTenantProfileCache.get(dummyTenant.getTenantId())).thenReturn(newProfile); - - assertNotEquals(oldProfileId, newProfileId); - - service.onTenantUpdate(dummyTenant.getTenantId()); - - assertEquals(newProfileId, tenantApiUsageState.getTenantProfileId()); - assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); - } - - private boolean containsFeature(ApiFeature feature) { - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { - return true; - } - } - return false; + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + return tenantProfile; } } \ No newline at end of file From 28ee01a7dd926db85ea9144e777d39583426b68a Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 24 Feb 2025 13:47:11 +0200 Subject: [PATCH 14/42] Fixed DefaultTbApiUsageStateServiceTest file formatting --- .../DefaultTbApiUsageStateServiceTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 8d4c1b849b..4e53cfa449 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -19,7 +19,10 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; @@ -76,10 +79,10 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { .setCustomerIdLSB(0) .setServiceId("testService"); - warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) - .setValue(VALUE_WARNING) - .build()); + warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) + .setValue(VALUE_WARNING) + .build()); TransportProtos.ToUsageStatsServiceMsg warningStatsMsg = warningMsgBuilder.build(); TbProtoQueueMsg warningMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), warningStatsMsg); @@ -94,10 +97,10 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { .setCustomerIdLSB(0) .setServiceId("testService"); - disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) - .setValue(VALUE_DISABLE) - .build()); + disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) + .setValue(VALUE_DISABLE) + .build()); TransportProtos.ToUsageStatsServiceMsg disableStatsMsg = disableMsgBuilder.build(); TbProtoQueueMsg disableMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), disableStatsMsg); From 545ea89713bdf3b4f31b3275d42b5c5f5e5575ae Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 24 Feb 2025 14:59:54 +0200 Subject: [PATCH 15/42] Increased timeout to 30 seconds --- .../org/thingsboard/server/service/apiusage/ApiUsageTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java index c708525a1d..84a6fb9931 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -94,7 +94,7 @@ public class ApiUsageTest extends AbstractControllerTest { doPostAsync(url, telemetryPayload, String.class, status().isOk()); } - await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2; @@ -102,7 +102,7 @@ public class ApiUsageTest extends AbstractControllerTest { doPostAsync(url, telemetryPayload, String.class, status().isOk()); } - await().atMost(5, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); }); From 3c137dec770e075e3e850ee41f4213b8d8f0c1d0 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 24 Feb 2025 16:19:58 +0200 Subject: [PATCH 16/42] TbDate - added functions to add years, months, days, etc. --- .../thingsboard/script/api/tbel/TbDate.java | 38 ++++++++++++ .../script/api/tbel/TbDateTest.java | 60 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index fa1c827c61..3b14876756 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -37,6 +37,7 @@ import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.Locale; import java.util.function.BiFunction; +import java.util.function.Function; public class TbDate implements Serializable, Cloneable { @@ -481,6 +482,43 @@ public class TbDate implements Serializable, Cloneable { instant = Instant.ofEpochMilli(dateMilliSecond); } + public void addDays(int days) { + adjustTime(zonedDateTime -> zonedDateTime.plusDays(days)); + } + + public void addYears(int years) { + adjustTime(zonedDateTime -> zonedDateTime.plusYears(years)); + } + + public void addMonths(int months) { + adjustTime(zonedDateTime -> zonedDateTime.plusMonths(months)); + } + + public void addWeeks(int weeks) { + adjustTime(zonedDateTime -> zonedDateTime.plusWeeks(weeks)); + } + + public void addHours(int hours) { + adjustTime(zonedDateTime -> zonedDateTime.plusHours(hours)); + } + + public void addMinutes(int minutes) { + adjustTime(zonedDateTime -> zonedDateTime.plusMinutes(minutes)); + } + + public void addSeconds(int seconds) { + adjustTime(zonedDateTime -> zonedDateTime.plusSeconds(seconds)); + } + + public void addNanos(long nanos) { + adjustTime(zonedDateTime -> zonedDateTime.plusNanos(nanos)); + } + + private void adjustTime(Function adjuster) { + ZonedDateTime zonedDateTime = adjuster.apply(getZonedDateTime()); + this.instant = zonedDateTime.toInstant(); + } + public ZoneOffset getLocaleZoneOffset(Instant... instants){ return ZoneId.systemDefault().getRules().getOffset(instants.length > 0 ? instants[0] : this.instant); } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index 439966536b..6c915a8258 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -885,4 +885,64 @@ class TbDateTest { Assertions.assertNotNull(serializedTbDate); Assertions.assertEquals(expectedDate.toString(), serializedTbDate); } + + @Test + void testAddFunctions() { + TbDate d = new TbDate(2024, 1, 1, 10, 0, 0, 0); + testResultChangeDateTime(d); + + d.addYears(1); + testResultChangeDateTime(d); + + d.addYears(-2); + testResultChangeDateTime(d); + + d.addMonths(2); + testResultChangeDateTime(d); + + d.addMonths(10); + testResultChangeDateTime(d); + + d.addMonths(-13); + testResultChangeDateTime(d); + + d.addWeeks(4); + testResultChangeDateTime(d); + + d.addWeeks(-5); + testResultChangeDateTime(d); + + d.addDays(6); + testResultChangeDateTime(d); + + d.addDays(45); + testResultChangeDateTime(d); + + d.addDays(-50); + testResultChangeDateTime(d); + + d.addHours(23); + testResultChangeDateTime(d); + + d.addHours(-47); + testResultChangeDateTime(d); + + d.addMinutes(59); + testResultChangeDateTime(d); + + d.addMinutes(-60); + testResultChangeDateTime(d); + + d.addSeconds(59); + testResultChangeDateTime(d); + + d.addSeconds(-60); + testResultChangeDateTime(d); + + d.addNanos(999999); + testResultChangeDateTime(d); + + d.addNanos(-1000000); + testResultChangeDateTime(d); + } } From 012aafbd82935eef0cad2f18ab55058b87497c7d Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Tue, 25 Feb 2025 12:02:25 +0200 Subject: [PATCH 17/42] Updated license --- .../org/thingsboard/server/service/apiusage/ApiUsageTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java index 84a6fb9931..961185d3cd 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * Copyright © 2016-2025 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. From 347e18ec4bf72c84b279f50a2a232710be3820be Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 25 Feb 2025 15:14:16 +0100 Subject: [PATCH 18/42] used correct queueName for TPI in input node --- .../actors/ruleChain/DefaultTbContext.java | 5 +- .../actors/rule/DefaultTbContextTest.java | 58 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 8bd3bcc81d..66afd2671c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,6 +124,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -178,7 +179,7 @@ public class DefaultTbContext implements TbContext { .resetRuleNodeId() .build(); tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(msg); doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t))); } @@ -195,7 +196,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); enqueue(tpi, tbMsg, onFailure, onSuccess); } diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index af2f9ce8ec..ea900afd4e 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -98,6 +98,56 @@ class DefaultTbContextTest { defaultTbContext = new DefaultTbContext(mainCtxMock, "Test rule chain name", nodeCtxMock); } + @MethodSource + @ParameterizedTest + public void givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .queueName(queueName) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + var ruleChainId = new RuleChainId(UUID.randomUUID()); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + + // WHEN + defaultTbContext.input(msg, ruleChainId); + + // THEN + + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + + private static Stream givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() { + return Stream.of("Main", "Test", null); + } + + private TopicPartitionInfo resolve(String queueName) { + var tpiBuilder = TopicPartitionInfo.builder() + .topic(queueName == null ? "MainQueueTopic" : queueName + "QueueTopic") + .partition(1) + .myPartition(true); + + return tpiBuilder.build(); + } + @Test public void givenDebugFailuresEvents_whenTellSuccess_thenVerifyDebugOutputNotPersisted() { // GIVEN @@ -810,10 +860,10 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest void givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(boolean debugFailures, - long debugAllUntil, - String connection, - boolean shouldPersist, - boolean shouldPersistAfterDurationTime) { + long debugAllUntil, + String connection, + boolean shouldPersist, + boolean shouldPersistAfterDurationTime) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); From 002ecc63424e8485271711df6c908cf425f2ae69 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 11:16:05 +0200 Subject: [PATCH 19/42] used correct queueName for TPI in input node --- .../server/actors/ruleChain/DefaultTbContext.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 0070eba4a1..091b97ac9d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,6 +124,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -175,7 +176,7 @@ public class DefaultTbContext implements TbContext { } TbMsg tbMsg = msg.copyWithRuleChainId(ruleChainId); tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(msg); doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t))); } @@ -192,7 +193,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); enqueue(tpi, tbMsg, onFailure, onSuccess); } From 9d20d34357c4c3a8fdc0e9869eb234a289d3a7e8 Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 16 Feb 2025 20:15:04 +0200 Subject: [PATCH 20/42] lwm2m: fix bug observe object with "ver = 1.1" (cherry picked from commit fcf74a82407ef54236e35eea97d6d838c531bc9f) --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 11 +- .../transport/lwm2m/Lwm2mTestHelper.java | 3 +- .../lwm2m/client/LwM2MTestClient.java | 21 +- ...LwM2MIntegrationObserve_Ver_1_0_Test.java} | 15 +- ...cLwM2MIntegrationObserve_Ver_1_1_Test.java | 28 ++ ...cLwM2MIntegrationObserve_Ver_1_2_Test.java | 28 ++ .../rpc/AbstractRpcLwM2MIntegrationTest.java | 6 +- ...cLwm2MIntegrationObserveCompositeTest.java | 4 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 4 +- ...cLwm2mIntegrationObserve_Ver_1_0_Test.java | 48 +++ ...cLwm2mIntegrationObserve_Ver_1_1_Test.java | 46 +++ ...cLwm2mIntegrationObserve_Ver_1_2_Test.java | 48 +++ .../src/test/resources/lwm2m/3-1_0.xml | 327 +++++++++++++++++ .../src/test/resources/lwm2m/3-1_1.xml | 331 ++++++++++++++++++ .../lwm2m/server/client/LwM2mClient.java | 40 ++- 15 files changed, 924 insertions(+), 36 deletions(-) rename application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/{AbstractRpcLwM2MIntegrationObserveTest.java => AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java} (50%) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java create mode 100644 application/src/test/resources/lwm2m/3-1_0.xml create mode 100644 application/src/test/resources/lwm2m/3-1_1.xml diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index a1fb723681..2c69ff6c97 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -89,6 +89,7 @@ import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSec; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -314,7 +315,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception { this.clientDestroy(false); - lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint); + lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); try (ServerSocket socket = new ServerSocket(0)) { int clientPort = socket.getLocalPort(); @@ -327,7 +328,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte private void clientDestroy(boolean isAfter) { try { - if (lwM2MTestClient != null) { + if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { if (isAfter) { sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); @@ -425,9 +426,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); return rpcActualResult.get("value").asText(); } + protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception { + String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params); + assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource")); + } + protected String sendRpcObserveOk(String method, String params) throws Exception { return sendObserveOK(method, params, lwM2MTestClient.getDeviceIdStr()); } + protected String sendObserveOK(String method, String params, String deviceIdStr) throws Exception { String sendRpcRequest; if (params == null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 4aa47eddf5..a40c937de8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -17,8 +17,9 @@ package org.thingsboard.server.transport.lwm2m; public class Lwm2mTestHelper { + public static final String[] lwm2mClientResources = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + // Models - public static final String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; public static final int BINARY_APP_DATA_CONTAINER = 19; public static final int TEMPERATURE_SENSOR = 3303; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 387085c511..a2dab420f6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -116,7 +116,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.setDtlsConnectorConfigCidLength; @@ -126,6 +126,7 @@ public class LwM2MTestClient { private final ScheduledExecutorService executor; private final String endpoint; + private final String[] modelResources; private LeshanClient leshanClient; private SimpleLwM2MDevice lwM2MDevice; private FwLwM2MDevice fwLwM2MDevice; @@ -147,10 +148,24 @@ public class LwM2MTestClient { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; - List models = new ArrayList<>(); - for (String resourceName : resources) { + + List models = ObjectLoader.loadAllDefault(); + for (String resourceName : lwm2mClientResources) { models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); } + if (this.modelResources != null) { + List modelsRes = new ArrayList<>(); + for (String resourceName : this.modelResources) { + modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); + } + Set idsToRemove = new HashSet<>(); + for (ObjectModel model : modelsRes) { + idsToRemove.add(model.id); + } + models.removeIf(model -> idsToRemove.contains(model.id)); + models.addAll(modelsRes); + } + LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); if (securityBs != null && security != null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java similarity index 50% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java index 7195b0d229..0b7faafd7b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java @@ -16,18 +16,13 @@ package org.thingsboard.server.transport.lwm2m.rpc; import org.thingsboard.server.dao.service.DaoSqlTest; -import static org.junit.Assert.assertTrue; @DaoSqlTest -public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{ - private final String[] RESOURCES_RPC_MULTIPLE_19 = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserveTest() { - setResources(this.RESOURCES_RPC_MULTIPLE_19); - } - - protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception { - String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params); - assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource")); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java new file mode 100644 index 0000000000..0a9290b0c4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 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.transport.lwm2m.rpc; + +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationTest{ + + public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_1.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java new file mode 100644 index 0000000000..e52ca15634 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2024 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.transport.lwm2m.rpc; + +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationTest{ + + public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index d5c9683c39..de4845a220 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -63,7 +63,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j @@ -102,7 +102,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; public AbstractRpcLwM2MIntegrationTest() { - setResources(resources); + setResources(lwm2mClientResources); } @Before @@ -110,7 +110,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) { supportFormatOnly_SenMLJSON_SenMLCBOR = true; } - if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationObserveTest")) { + if (this.getClass().getSimpleName().contains("RpcLwm2mIntegrationObserve")) { initRpc(0); } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) { initRpc(3303); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java index 56de08e66d..aa05b8073c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -50,7 +50,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_IN import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j -public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { +public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationTest { /** * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 57213e08cd..425c7a3d6b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -24,7 +24,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; @@ -41,7 +41,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j -public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest { +public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { @Before public void setupObserveTest() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java new file mode 100644 index 0000000000..9c2fbd3313 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; + +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.0/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java new file mode 100644 index 0000000000..79d09721e9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2024 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.1/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java new file mode 100644 index 0000000000..ec45c2beac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test; + +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.2/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/resources/lwm2m/3-1_0.xml b/application/src/test/resources/lwm2m/3-1_0.xml new file mode 100644 index 0000000000..4f5261d263 --- /dev/null +++ b/application/src/test/resources/lwm2m/3-1_0.xml @@ -0,0 +1,327 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0-7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + mV + + + + Power Source Current + R + Multiple + Optional + Integer + + mA + + + + Battery Level + R + Single + Optional + Integer + 0-100 + % + + + + Memory Free + R + Single + Optional + Integer + + KB + + + + Error Code + R + Multiple + Mandatory + Integer + 0-8 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0-6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/application/src/test/resources/lwm2m/3-1_1.xml b/application/src/test/resources/lwm2m/3-1_1.xml new file mode 100644 index 0000000000..01151a57b7 --- /dev/null +++ b/application/src/test/resources/lwm2m/3-1_1.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.1 + 1.1 + 1.1 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + % + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..8 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 713d5b9fa8..3146eba95b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -24,6 +24,9 @@ import org.eclipse.leshan.core.LwM2m; import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.attributes.Attribute; +import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -453,22 +456,33 @@ public class LwM2mClient { } private void setSupportedClientObjects(){ - this.supportedClientObjects = new ConcurrentHashMap<>(); - for (Link link: this.registration.getSortedObjectLinks()) { - LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference()); - if (lwM2mPath.isObject()) { - LwM2m.Version ver; - if (link.getAttributes().get("ver")!= null) { - ver = new LwM2m.Version(link.getAttributes().get("ver").getValue().toString()); - } else { - ver = getDefaultObjectIDVer(); + if (this.registration.getSupportedObject() != null && this.registration.getSupportedObject().size() > 0) { + this.supportedClientObjects = this.registration.getSupportedObject(); + } else { + this.supportedClientObjects = new ConcurrentHashMap<>(); + for (Link link : this.registration.getSortedObjectLinks()) { + if (link instanceof MixedLwM2mLink) { + LwM2mPath path = ((MixedLwM2mLink) link).getPath(); + // add supported objects + if (path.isObject() || path.isObjectInstance()) { + int objectId = path.getObjectId(); + LwM2mAttribute versionParamValue = link.getAttributes().get(LwM2mAttributes.OBJECT_VERSION); + if (versionParamValue != null) { + // if there is a version attribute then use it as version for this object + this.supportedClientObjects.put(objectId, versionParamValue.getValue()); + } else { + // there is no version attribute attached. + // In this case we use the DEFAULT_VERSION only if this object stored as supported object. + Version currentVersion = this.supportedClientObjects.get(objectId); + if (currentVersion == null) { + this.supportedClientObjects.put(objectId, getDefaultObjectIDVer()); + } + } + } } - this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); - } else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ - this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } } } - } + From 01e99efc4ed6e59c21c551ab0e5da11083d27fea Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 12:28:01 +0200 Subject: [PATCH 21/42] Minor refactoring for TbContext --- .../server/actors/ruleChain/DefaultTbContext.java | 5 ++--- .../thingsboard/rule/engine/api/TbContext.java | 15 ++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 091b97ac9d..24edd8f8bf 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.Arrays; +import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; @@ -64,7 +65,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbActorMsg; @@ -193,8 +193,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); - enqueue(tpi, tbMsg, onFailure, onSuccess); + enqueue(tbMsg, MAIN_QUEUE_NAME, onSuccess, onFailure); } @Override diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index b517bb0051..b3fc646b94 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -144,11 +144,19 @@ public interface TbContext { /** * Puts new message to queue for processing by the Root Rule Chain + * WARNING: message is put to the Main queue. To specify other queue name - use {@link #enqueue(TbMsg, String, Runnable, Consumer)} * * @param msg - message */ void enqueue(TbMsg msg, Runnable onSuccess, Consumer onFailure); + /** + * Puts new message to custom queue for processing + * + * @param msg - message + */ + void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer onFailure); + /** * Sends message to the nested rule chain. * Fails processing of the message if the nested rule chain is not found. @@ -167,13 +175,6 @@ public interface TbContext { */ void output(TbMsg msg, String relationType); - /** - * Puts new message to custom queue for processing - * - * @param msg - message - */ - void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer onFailure); - void enqueueForTellFailure(TbMsg msg, String failureMessage); void enqueueForTellFailure(TbMsg tbMsg, Throwable t); From 45db2518171dba1eb9bf8ea85ebd36dfe0a24466 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 26 Feb 2025 12:56:07 +0100 Subject: [PATCH 22/42] used queueName from TbMsg fro enqueue --- .../actors/ruleChain/DefaultTbContext.java | 3 +- .../actors/rule/DefaultTbContextTest.java | 76 +++++++++++++++++++ .../rule/engine/api/TbContext.java | 3 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index cd8521027a..a1e8434b6e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,7 +124,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; -import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -196,7 +195,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - enqueue(tbMsg, MAIN_QUEUE_NAME, onSuccess, onFailure); + enqueue(tbMsg, tbMsg.getQueueName(), onSuccess, onFailure); } @Override diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index ea900afd4e..54205327ca 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -131,11 +131,87 @@ class DefaultTbContextTest { defaultTbContext.input(msg, ruleChainId); // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + + @MethodSource + @ParameterizedTest + public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .queueName(queueName) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, () -> {}, t -> {}); + + // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + @MethodSource + @ParameterizedTest + public void givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, queueName, () -> {}, t -> {}); + + // THEN then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); } private static Stream givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream testQueueNames() { return Stream.of("Main", "Test", null); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d743867b84..46b63e0595 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -143,8 +143,7 @@ public interface TbContext { void tellFailure(TbMsg msg, Throwable th); /** - * Puts new message to queue for processing by the Root Rule Chain - * WARNING: message is put to the Main queue. To specify other queue name - use {@link #enqueue(TbMsg, String, Runnable, Consumer)} + * Puts new message to queue from TbMsg for processing by the Root Rule Chain * * @param msg - message */ From 2c710535115f0212b2664d01727695c1ef8fe787 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 15:48:20 +0200 Subject: [PATCH 23/42] Fix double version increment when creating entity in some cases --- .../DeviceProfileControllerTest.java | 74 +++++++++++++------ .../server/dao/sql/JpaAbstractDao.java | 20 ++--- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index b549d2af2a..663f5b9cde 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -60,6 +61,7 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; @@ -79,6 +81,8 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Autowired private DeviceProfileDao deviceProfileDao; + static final String LWM2M_PROFILE_JSON = "{\"name\":\"lwm2m profile\",\"type\":\"DEFAULT\",\"image\":null,\"defaultQueueName\":null,\"transportType\":\"LWM2M\",\"provisionType\":\"DISABLED\",\"description\":\"\",\"profileData\":{\"configuration\":{\"type\":\"DEFAULT\"},\"transportConfiguration\":{\"observeAttr\":{\"observe\":[],\"attribute\":[],\"telemetry\":[\"/11_1.1/0/0\"],\"keyName\":{\"/11_1.1/0/0\":\"profileName\"},\"attributeLwm2m\":{}},\"bootstrap\":[{\"shortServerId\":123,\"bootstrapServerIs\":false,\"host\":\"0.0.0.0\",\"port\":5685,\"clientHoldOffTime\":1,\"serverPublicKey\":\"\",\"serverCertificate\":\"\",\"bootstrapServerAccountTimeout\":0,\"lifetime\":300,\"defaultMinPeriod\":1,\"notifIfDisabled\":true,\"binding\":\"U\",\"securityMode\":\"NO_SEC\"}],\"clientLwM2mSettings\":{\"clientOnlyObserveAfterConnect\":1,\"fwUpdateStrategy\":1,\"swUpdateStrategy\":1,\"powerMode\":\"DRX\",\"edrxCycle\":81000,\"psmActivityTimer\":10000,\"pagingTransmissionWindow\":10000,\"defaultObjectIDVer\":\"1.0\"},\"bootstrapServerUpdateEnable\":false,\"type\":\"LWM2M\"},\"alarms\":null,\"provisionConfiguration\":{\"type\":\"DISABLED\"}}}"; + static class Config { @Bean @Primary @@ -119,7 +123,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertNotNull(savedDeviceProfile.getId()); Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); @@ -135,7 +139,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ActionType.ADDED); savedDeviceProfile.setName("New device profile"); - doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class); + saveDeviceProfile(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); @@ -162,7 +166,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertNotNull(foundDeviceProfile); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -171,7 +175,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); @@ -183,7 +187,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileInfoById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/" + savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); Assert.assertNotNull(foundDeviceProfileInfo); Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); @@ -213,7 +217,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); doGet("/api/deviceProfileInfo/" + deviceProfile.getId()) @@ -235,7 +239,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testSetDefaultDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -328,7 +332,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTypeNull() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -345,7 +349,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); device.setType("default"); @@ -367,7 +371,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithExistingDevice() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); @@ -419,7 +423,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithFirmwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(differentProfile.getId()); firmwareInfo.setType(FIRMWARE); @@ -441,7 +445,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithSoftwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest softwareInfo = new SaveOtaPackageInfoRequest(); softwareInfo.setDeviceProfileId(differentProfile.getId()); softwareInfo.setType(SOFTWARE); @@ -462,7 +466,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -495,7 +499,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { int cntEntity = 28; for (int i = 0; i < cntEntity; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new DeviceProfile(), new DeviceProfile(), @@ -552,7 +556,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { for (int i = 0; i < 28; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } List loadedDeviceProfileInfos = new ArrayList<>(); @@ -961,7 +965,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { JsonTransportPayloadConfiguration jsonTransportPayloadConfiguration = new JsonTransportPayloadConfiguration(); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(jsonTransportPayloadConfiguration, true); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -979,7 +983,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { "v1/devices/me/telemetry", "v1/devices/me/attributes", "v1/devices/me/subscribeattributes"); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -997,7 +1001,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema, null, null); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration, false); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -1036,14 +1040,14 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithDeleteRelationsOk() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @Ignore @Test public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @@ -1103,8 +1107,36 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(count, deviceProfileNames.size()); } - private DeviceProfile savedDeviceProfile(String name) { + @Test + public void testSaveDeviceProfileWithOutdatedVersion() throws Exception { + DeviceProfile deviceProfile = JacksonUtil.fromString(LWM2M_PROFILE_JSON, DeviceProfile.class); + deviceProfile.setName("Device profile v1.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isOne(); + + deviceProfile.setName("Device profile v2.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isEqualTo(2); + + deviceProfile.setName("Device profile v1.1"); + deviceProfile.setVersion(1L); + String response = doPost("/api/deviceProfile", deviceProfile).andExpect(status().isConflict()) + .andReturn().getResponse().getContentAsString(); + assertThat(JacksonUtil.toJsonNode(response).get("message").asText()) + .containsIgnoringCase("already changed by someone else"); + + deviceProfile.setVersion(null); // overriding entity + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getName()).isEqualTo("Device profile v1.1"); + assertThat(deviceProfile.getVersion()).isEqualTo(3); + } + + private DeviceProfile saveDeviceProfile(String name) { DeviceProfile deviceProfile = createDeviceProfile(name); + return saveDeviceProfile(deviceProfile); + } + + private DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); } 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 f044629364..0163291aba 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 @@ -88,10 +88,10 @@ public abstract class JpaAbstractDao, D> boolean flushed = false; EntityManager entityManager = getEntityManager(); if (isNew) { + entityManager.persist(entity); if (entity instanceof HasVersion versionedEntity) { versionedEntity.setVersion(1L); } - entityManager.persist(entity); } else { if (entity instanceof HasVersion versionedEntity) { if (versionedEntity.getVersion() == null) { @@ -106,23 +106,25 @@ public abstract class JpaAbstractDao, D> } } versionedEntity = entityManager.merge(versionedEntity); + entity = (E) versionedEntity; /* * by default, Hibernate doesn't issue an update query and thus version increment * if the entity was not modified. to bypass this and always increment the version, we do it manually * */ versionedEntity.setVersion(versionedEntity.getVersion() + 1); - /* - * flushing and then removing the entity from the persistence context so that it is not affected - * by next flushes (e.g. when a transaction is committed) to avoid double version increment - * */ - entityManager.flush(); - entityManager.detach(versionedEntity); - flushed = true; - entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } } + if (entity instanceof HasVersion versionedEntity) { + /* + * flushing and then removing the entity from the persistence context so that it is not affected + * by next flushes (e.g. when a transaction is committed) to avoid double version increment + * */ + entityManager.flush(); + entityManager.detach(versionedEntity); + flushed = true; + } if (flush && !flushed) { entityManager.flush(); } From e372587926fc2fbf152df3c4f54fdf7121bca8c8 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Feb 2025 13:01:21 +0200 Subject: [PATCH 24/42] Edge rule chain CRUD: fixed setting root rule chain --- .../processor/edge/EdgeEntityProcessor.java | 4 +++ .../rule/BaseRuleChainProcessor.java | 13 +++++--- .../rule/RuleChainEdgeProcessor.java | 33 +++++++++++-------- .../entitiy/EntityStateSourcingListener.java | 5 +++ .../server/edge/AbstractEdgeTest.java | 28 +++++++--------- .../server/edge/RuleChainEdgeTest.java | 1 + 6 files changed, 49 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java index ddbe4810df..77fa31c028 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java @@ -49,8 +49,12 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor { @Override public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { try { + EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); EdgeId edgeId = new EdgeId(new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + if (edgeId.equals(originatorEdgeId)) { + return Futures.immediateFuture(null); + } switch (actionType) { case ASSIGNED_TO_CUSTOMER: { CustomerId customerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), CustomerId.class); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 03904ce5ef..3231d7788a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -39,24 +39,27 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { protected Pair saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, RuleChainType ruleChainType) { boolean created = false; - RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); - if (ruleChain == null) { + RuleChain ruleChainFromDb = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); + if (ruleChainFromDb == null) { created = true; } - ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); + RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true); if (ruleChain == null) { throw new RuntimeException("[{" + tenantId + "}] ruleChainUpdateMsg {" + ruleChainUpdateMsg + "} cannot be converted to rule chain"); } boolean isRoot = ruleChain.isRoot(); - ruleChain.setRoot(false); + if (RuleChainType.CORE.equals(ruleChainType)) { + ruleChain.setRoot(false); + } else { + ruleChain.setRoot(ruleChainFromDb == null ? false : ruleChainFromDb.isRoot()); + } ruleChain.setType(ruleChainType); ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); if (created) { ruleChain.setId(ruleChainId); } - edgeCtx.getRuleChainService().saveRuleChain(ruleChain); return Pair.of(created, isRoot); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 340fb09eab..06fb4c37a2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -57,8 +57,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { switch (ruleChainUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: - saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); - return Futures.immediateFuture(null); + return saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, edge); case ENTITY_DELETED_RPC_MESSAGE: RuleChain ruleChainToDelete = edgeCtx.getRuleChainService().findRuleChainById(tenantId, ruleChainId); if (ruleChainToDelete != null) { @@ -81,19 +80,25 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } } - private void saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { - Pair resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); - Boolean created = resultPair.getFirst(); - if (created) { - createRelationFromEdge(tenantId, edge.getId(), ruleChainId); - pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity()); - edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); - } - Boolean isRoot = resultPair.getSecond(); - if (isRoot) { - edge.setRootRuleChainId(ruleChainId); - edgeCtx.getEdgeService().saveEdge(edge); + private ListenableFuture saveOrUpdateRuleChain(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateMsg ruleChainUpdateMsg, Edge edge) { + try { + Pair resultPair = super.saveOrUpdateRuleChain(tenantId, ruleChainId, ruleChainUpdateMsg, RuleChainType.EDGE); + Boolean created = resultPair.getFirst(); + if (created) { + createRelationFromEdge(tenantId, edge.getId(), ruleChainId); + pushRuleChainCreatedEventToRuleEngine(tenantId, edge, ruleChainId, ruleChainUpdateMsg.getEntity()); + edgeCtx.getRuleChainService().assignRuleChainToEdge(tenantId, ruleChainId, edge.getId()); + } + Boolean isRoot = resultPair.getSecond(); + if (isRoot) { + edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edge.getId()); + edgeCtx.getEdgeService().setEdgeRootRuleChain(tenantId, edge, ruleChainId); + } + } catch (Exception e) { + log.error("Failed to save or update rule chain", e); + return Futures.immediateFailedFuture(e); } + return Futures.immediateFuture(null); } private void pushRuleChainCreatedEventToRuleEngine(TenantId tenantId, Edge edge, RuleChainId ruleChainId, String ruleChainAsString) { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 35aad36dff..e40251673d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -64,6 +65,7 @@ public class EntityStateSourcingListener { private final TenantService tenantService; private final TbClusterService tbClusterService; + private final EdgeSynchronizationManager edgeSynchronizationManager; @PostConstruct public void init() { @@ -241,6 +243,9 @@ public class EntityStateSourcingListener { private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) { if (entity instanceof Edge) { + if (entityId.equals(edgeSynchronizationManager.getEdgeId().get())) { + return; + } tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent)); } else if (entity instanceof EdgeEvent edgeEvent) { tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId())); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 44dac215e2..feac7adb04 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -140,7 +140,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(27); + edgeImitator.expectMessageAmount(25); edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); edgeImitator.connect(); @@ -191,8 +191,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private RuleChainId getEdgeRootRuleChainId() throws Exception { - List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", - new TypeReference>() {}, new PageLink(100)).getData(); + List edgeRuleChains = doGetTypedWithPageLink("/api/ruleChains?type={type}&", + new TypeReference>() {}, + new PageLink(100, 0, "Edge Root Rule Chain"), + "EDGE").getData(); for (RuleChain edgeRuleChain : edgeRuleChains) { if (edgeRuleChain.isRoot()) { return edgeRuleChain.getId(); @@ -243,8 +245,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(RuleChainUpdateMsg.class, 1); UUID ruleChainUUID = validateRuleChains(); - // 2 messages - 1 from rule chain fetcher and 1 from rule chain controller (it goes along with RuleChainUpdateMsg) - validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2); + // 1 from rule chain fetcher + validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1); validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages ('general', 'mail', 'connectivity', 'jwt') @@ -426,17 +428,11 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private void validateRuleChainMetadataUpdates(UUID expectedRuleChainUUID) { - List ruleChainMetadataUpdateMsgList = edgeImitator.findAllMessagesByType(RuleChainMetadataUpdateMsg.class); - Assert.assertEquals(2, ruleChainMetadataUpdateMsgList.size()); - // metadata create msg - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgCreated = ruleChainMetadataUpdateMsgList.get(0); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgCreated.getMsgType()); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsgCreated.getEntity(), RuleChainMetaData.class, true); - Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId()); - // metadata update msg - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsgUpdated = ruleChainMetadataUpdateMsgList.get(1); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, ruleChainMetadataUpdateMsgUpdated.getMsgType()); - ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsgUpdated.getEntity(), RuleChainMetaData.class, true); + Optional ruleChainMetadataUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class); + Assert.assertTrue(ruleChainMetadataUpdateMsgOpt.isPresent()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateMsgOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); Assert.assertEquals(expectedRuleChainUUID, ruleChainMetaData.getRuleChainId().getId()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index ed77c4b97a..7c029e0471 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -105,6 +105,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // create rule chain on edge RuleChain edgeRuleChain = new RuleChain(); + edgeRuleChain.setTenantId(tenantId); edgeRuleChain.setId(new RuleChainId(uuid)); edgeRuleChain.setName(ruleChainName); UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); From b29b192efc78a3b2af9bed0679f111bf39be2c10 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Feb 2025 13:20:59 +0200 Subject: [PATCH 25/42] Edge Rule Chain CRUD: revert metadata validatino --- .../rule/BaseRuleChainProcessor.java | 8 +++++- .../server/dao/rule/RuleChainService.java | 2 -- .../server/dao/rule/BaseRuleChainService.java | 26 +++++-------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 3231d7788a..5156956c3d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; @@ -70,7 +71,12 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { throw new RuntimeException("[{" + tenantId + "}] ruleChainMetadataUpdateMsg {" + ruleChainMetadataUpdateMsg + "} cannot be converted to rule chain metadata"); } if (!ruleChainMetadata.getNodes().isEmpty()) { - edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true, false); + ruleChainMetadata.setVersion(null); + for (RuleNode ruleNode : ruleChainMetadata.getNodes()) { + ruleNode.setRuleChainId(null); + ruleNode.setId(null); + } + edgeCtx.getRuleChainService().saveRuleChainMetaData(tenantId, ruleChainMetadata, Function.identity(), true); } } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index 7558d49971..a2356ee149 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -54,8 +54,6 @@ public interface RuleChainService extends EntityDaoService { RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent); - RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent, boolean doValidate); - RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId); RuleChain findRuleChainById(TenantId tenantId, RuleChainId ruleChainId); 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 685a25a41e..b7325ed714 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 @@ -174,25 +174,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater) { - return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true, true); + return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, true); } @Transactional @Override public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { - return saveRuleChainMetaData(tenantId, ruleChainMetaData, ruleNodeUpdater, publishSaveEvent, true); - } - - @Transactional - @Override - public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, - boolean publishSaveEvent, boolean doValidate) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { return RuleChainUpdateResult.failed(); - } else if (doValidate && ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { + } else if (ruleChainMetaData.getVersion() != null && !ruleChainMetaData.getVersion().equals(ruleChain.getVersion())) { throw new EntityVersionMismatchException(EntityType.RULE_CHAIN, null); } RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); @@ -206,14 +199,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (nodes != null) { for (RuleNode node : nodes) { setSingletonMode(node); - if (doValidate) { - if (node.getId() != null) { - ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); - } else { - toAddOrUpdate.add(node); - } - } else { + if (node.getId() != null) { ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); + } else { toAddOrUpdate.add(node); } } @@ -226,10 +214,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { - if (doValidate) { - newRuleNode = ruleChainMetaData.getNodes().get(index); - toAddOrUpdate.add(newRuleNode); - } + newRuleNode = ruleChainMetaData.getNodes().get(index); + toAddOrUpdate.add(newRuleNode); } else { updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, null)); toDelete.add(existingNode); From cc925a75c42e50d8978f9ade2c7487d6063be704 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 27 Feb 2025 13:41:17 +0200 Subject: [PATCH 26/42] edge.proto - mark deprecated fields --- common/edge-api/src/main/proto/edge.proto | 25 +++++++++++++------ .../server/dao/rule/BaseRuleChainService.java | 3 +-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 4058260629..023ac00634 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -42,6 +42,8 @@ enum EdgeVersion { V_3_8_0 = 8; V_3_9_0 = 9; V_4_0_0 = 10; + + V_LATEST = 999; } /** @@ -303,8 +305,9 @@ message NotificationTemplateUpdateMsg { optional string entity = 4; } -// deprecated +// DEPRECATED. FOR REMOVAL message RuleChainMetadataRequestMsg { + option deprecated = true; int64 ruleChainIdMSB = 1; int64 ruleChainIdLSB = 2; } @@ -322,22 +325,30 @@ message RelationRequestMsg { string entityType = 3; } +// DEPRECATED. FOR REMOVAL message UserCredentialsRequestMsg { + option deprecated = true; int64 userIdMSB = 1; int64 userIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message DeviceCredentialsRequestMsg { + option deprecated = true; int64 deviceIdMSB = 1; int64 deviceIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message WidgetBundleTypesRequestMsg { + option deprecated = true; int64 widgetBundleIdMSB = 1; int64 widgetBundleIdLSB = 2; } +// DEPRECATED. FOR REMOVAL message EntityViewsRequestMsg { + option deprecated = true; int64 entityIdMSB = 1; int64 entityIdLSB = 2; string entityType = 3; @@ -395,14 +406,14 @@ message UplinkMsg { repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 4; repeated AlarmUpdateMsg alarmUpdateMsg = 5; repeated RelationUpdateMsg relationUpdateMsg = 6; - repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7; // deprecated + repeated RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg = 7 [deprecated = true]; repeated AttributesRequestMsg attributesRequestMsg = 8; repeated RelationRequestMsg relationRequestMsg = 9; - repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10; - repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11; + repeated UserCredentialsRequestMsg userCredentialsRequestMsg = 10 [deprecated = true]; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 11 [deprecated = true]; repeated DeviceRpcCallMsg deviceRpcCallMsg = 12; - repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; - repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; + repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14 [deprecated = true]; + repeated EntityViewsRequestMsg entityViewsRequestMsg = 15 [deprecated = true]; repeated AssetUpdateMsg assetUpdateMsg = 16; repeated DashboardUpdateMsg dashboardUpdateMsg = 17; repeated EntityViewUpdateMsg entityViewUpdateMsg = 18; @@ -430,7 +441,7 @@ message DownlinkMsg { int32 downlinkMsgId = 1; SyncCompletedMsg syncCompletedMsg = 2; repeated EntityDataProto entityData = 3; - repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4; + repeated DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = 4 [deprecated = true]; repeated DeviceUpdateMsg deviceUpdateMsg = 5; repeated DeviceProfileUpdateMsg deviceProfileUpdateMsg = 6; repeated DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = 7; 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 b7325ed714..b5fde88a0b 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 @@ -179,8 +179,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Transactional @Override - public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, - boolean publishSaveEvent) { + public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function ruleNodeUpdater, boolean publishSaveEvent) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { From 2439af70c831fc25512bd48894563f4ae3a8b664 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Feb 2025 13:58:56 +0100 Subject: [PATCH 27/42] removed unnecessary coap configs --- .../java/org/thingsboard/server/msa/ContainerTestSuite.java | 2 ++ transport/coap/src/main/resources/tb-coap-transport.yml | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 77567163c6..3c067ee641 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -178,6 +178,8 @@ public class ContainerTestSuite { .waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-coap-transport", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) + .waitingFor("tb-lwm2m-transport", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)) .waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT)); diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 5740f199f8..f1d325b81c 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -133,8 +133,6 @@ redis: transport: # Local CoAP transport parameters coap: - # Enable/disable coap transport protocol. - enabled: "${COAP_ENABLED:true}" # CoaP processing timeout in milliseconds timeout: "${COAP_TIMEOUT:10000}" # CoaP piggyback response timeout in milliseconds @@ -173,8 +171,6 @@ transport: # CoAP server parameters coap: - # Enable/disable coap transport protocol. - enabled: "${COAP_SERVER_ENABLED:true}" # CoAP bind-address bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" # CoAP bind port From 64e1fc1ce63f1920a7a06d9b438ea92014b396ba Mon Sep 17 00:00:00 2001 From: artem-barysh-dev Date: Thu, 27 Feb 2025 17:34:45 +0200 Subject: [PATCH 28/42] Added validation for resource usage across entities (#12508) * Added Validation for Resource Usage Across Entities * Fixed table name in native queries * resolved issues from PR review * UI: Feature show JS resources usage message warning on delete. Refactored images-in-use-dialog. * UI: JS lib table fix display delete icon. * UI: Resources in use minor improvements. * Fixed tests, license format * Fix issues causing test failures * Removed unused method * UI: Fix js-library table cell actions. * UI: Fix js-library table cell actions. --------- Co-authored-by: deaflynx --- .../controller/TbResourceController.java | 9 +- .../resource/DefaultTbResourceService.java | 11 +- .../service/resource/TbResourceService.java | 5 +- .../controller/TbResourceControllerTest.java | 202 ++++++++++++++-- .../sql/BaseTbResourceServiceTest.java | 221 +++++++++++++++++- .../server/dao/resource/ResourceService.java | 5 +- .../common/data/TbResourceDeleteResult.java | 32 +++ .../server/dao/ResourceContainerDao.java | 29 +++ .../dao/dashboard/DashboardInfoDao.java | 3 +- .../server/dao/resource/BaseImageService.java | 12 +- .../dao/resource/BaseResourceService.java | 60 ++++- .../validator/ResourceDataValidator.java | 15 -- .../dashboard/DashboardInfoRepository.java | 16 +- .../sql/dashboard/JpaDashboardInfoDao.java | 11 + .../dao/sql/widget/JpaWidgetTypeDao.java | 15 +- .../sql/widget/WidgetTypeInfoRepository.java | 15 +- .../dao/sql/widget/WidgetTypeRepository.java | 6 - .../server/dao/widget/WidgetTypeDao.java | 5 +- ui-ngx/src/app/core/http/resource.service.ts | 4 +- .../js-library-table-config.resolver.ts | 191 +++++++++++++-- .../admin/resource/resources-datasource.ts | 130 +++++++++++ .../image/image-gallery.component.ts | 68 ++++-- .../image/image-references.component.ts | 10 +- .../resources-in-use-dialog.component.html} | 42 ++-- .../resources-in-use-dialog.component.scss} | 2 +- .../resources-in-use-dialog.component.ts} | 61 +++-- .../src/app/shared/models/resource.models.ts | 24 +- ui-ngx/src/app/shared/shared.module.ts | 6 +- .../assets/locale/locale.constant-en_US.json | 5 + 29 files changed, 1012 insertions(+), 203 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts rename ui-ngx/src/app/shared/components/{image/images-in-use-dialog.component.html => resource/resources-in-use-dialog.component.html} (71%) rename ui-ngx/src/app/shared/components/{image/images-in-use-dialog.component.scss => resource/resources-in-use-dialog.component.scss} (96%) rename ui-ngx/src/app/shared/components/{image/images-in-use-dialog.component.ts => resource/resources-in-use-dialog.component.ts} (66%) diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 9c72b9a5ad..b23603f6a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -322,12 +323,14 @@ public class TbResourceController extends BaseController { notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @DeleteMapping(value = "/resource/{resourceId}") - public void deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) - @PathVariable("resourceId") String strResourceId) throws ThingsboardException { + public ResponseEntity deleteResource(@Parameter(description = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable("resourceId") String strResourceId, + @RequestParam(name = "force", required = false) boolean force) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); TbResource tbResource = checkResourceId(resourceId, Operation.DELETE); - tbResourceService.delete(tbResource, getCurrentUser()); + TbResourceDeleteResult tbResourceDeleteResult = tbResourceService.delete(tbResource, force, getCurrentUser()); + return (tbResourceDeleteResult.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(tbResourceDeleteResult); } private ResponseEntity downloadResourceIfChanged(String strResourceId, String etag) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 1c55784f5c..634190c1d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -89,7 +90,7 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } @Override - public void delete(TbResource tbResource, User user) { + public TbResourceDeleteResult delete(TbResourceInfo tbResource, boolean force, User user) { if (tbResource.getResourceType() == ResourceType.IMAGE) { throw new IllegalArgumentException("Image resource type is not supported"); } @@ -97,8 +98,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceId resourceId = tbResource.getId(); TenantId tenantId = tbResource.getTenantId(); try { - resourceService.deleteResource(tenantId, resourceId); - logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); + TbResourceDeleteResult result = resourceService.deleteResource(tenantId, resourceId, force); + if (result.isSuccess()) { + logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); + } + + return result; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), actionType, user, e, resourceId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index 61a870ccb7..2e6e43e7bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.service.resource; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; @@ -37,7 +38,7 @@ public interface TbResourceService { TbResourceInfo save(TbResource entity, SecurityUser user) throws Exception; - void delete(TbResource entity, User user); + TbResourceDeleteResult delete(TbResourceInfo entity, boolean force, User user); List findLwM2mObject(TenantId tenantId, String sortOrder, diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index c4074f0d00..0d7b81370a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -17,6 +17,7 @@ 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 org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Assert; @@ -27,7 +28,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; @@ -40,13 +44,16 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -63,7 +70,6 @@ public class TbResourceControllerTest extends AbstractControllerTest { private static final String JS_TEST_FILE_NAME = "test.js"; private static final String TEST_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; - private Tenant savedTenant; private User tenantAdmin; @@ -222,28 +228,196 @@ public class TbResourceControllerTest extends AbstractControllerTest { } @Test - public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception { + public void testUnForcedDeleteTbResourceIfAssignedToWidgetType() throws Exception { TbResource resource = new TbResource(); - resource.setResourceType(ResourceType.JKS); + resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("My first resource"); - resource.setFileName(DEFAULT_FILE_NAME); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); TbResourceInfo savedResource = save(resource); - Mockito.reset(tbClusterService, auditLogService); - String resourceIdStr = savedResource.getId().getId().toString(); + var link = resource.getLink(); + WidgetTypeDetails widgetType = new WidgetTypeDetails(); + widgetType.setName("Widget Type"); + widgetType.setTenantId(savedTenant.getId()); + widgetType.setDescriptor(JacksonUtil.newObjectNode() + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config") + .put("resource", link)); + WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=false") + .andExpect(status().isBadRequest()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertFalse(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + Assert.assertNotNull(referenceValues); + + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNotNull(widgetTypeInfos); + Assert.assertFalse(widgetTypeInfos.isEmpty()); + Assert.assertEquals(1, widgetTypeInfos.size()); + + var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); + Assert.assertNotNull(dashboardInfo); - //create widget type + WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class); + Assert.assertNotNull(foundedWidgetType); + Assert.assertEquals(foundedWidgetType, dashboardInfo); + } + + @Test + public void testForcedDeleteTbResourceIfAssignedToWidgetType() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setName("Widget Type"); - widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":\"tb-resource;/api/resource/jks/tenant/%s\",\"isModule\":true}]}", savedResource.getResourceKey()), JsonNode.class)); + widgetType.setTenantId(savedTenant.getId()); + widgetType.setDescriptor(JacksonUtil.newObjectNode() + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config") + .put("resource", link)); doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); - doDelete("/api/resource/" + resourceIdStr) + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=true") + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertTrue(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNull(widgetTypeInfos); + } + + @Test + public void testUnForcedDeleteTbResourceIfAssignedToDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("resource", link)); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=false") .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Following widget types use this resource: " - + widgetType.getName()))); + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertFalse(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + Assert.assertNotNull(referenceValues); + + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNotNull(dashboardInfos); + Assert.assertFalse(dashboardInfos.isEmpty()); + Assert.assertEquals(1, dashboardInfos.size()); + + var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); + Assert.assertNotNull(dashboardInfo); + + DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class); + Assert.assertNotNull(foundDashboard); + Assert.assertEquals(foundDashboard, dashboardInfo); + } + + @Test + public void testForcedDeleteTbResourceIfAssignedToDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My first resource"); + resource.setFileName(JS_TEST_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setEncodedData(TEST_DATA); + resource.setResourceKey(JS_TEST_FILE_NAME); + TbResourceInfo savedResource = save(resource); + + var link = resource.getLink(); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("resource", link)); + doPost("/api/dashboard", dashboard, Dashboard.class); + + var deleteResponse = doDelete("/api/resource/" + savedResource.getUuidId() + "?force=true") + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + Assert.assertNotNull(deleteResponse); + + boolean isSuccess = JacksonUtil.toJsonNode(deleteResponse).get("success").asBoolean(); + Assert.assertTrue(isSuccess); + + var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + }); + Assert.assertNull(dashboardInfos); } @Test @@ -676,7 +850,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() {}); + doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() { + }); Assert.assertNotNull(objects); Assert.assertEquals(resources.size(), objects.size()); @@ -690,7 +865,8 @@ public class TbResourceControllerTest extends AbstractControllerTest { List resources = loadLwm2mResources(); List objects = - doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() {}); + doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.2,5_1.2,19_1.1", new TypeReference<>() { + }); Assert.assertNotNull(objects); Assert.assertEquals(3, objects.size()); diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index 2ec917c81a..fe416eacd5 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -16,15 +16,22 @@ package org.thingsboard.server.service.resource.sql; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; @@ -36,10 +43,14 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.resource.TbResourceService; import java.util.ArrayList; @@ -108,6 +119,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { ""; private static final String DEFAULT_FILE_NAME = "test.jks"; + private static final String JS_FILE_NAME = "test.js"; private static final String TEST_BASE64_DATA = "77u/PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQpGSUxFIElORk9STUFUSU9OCgpPTUEgUGVybWFuZW50IERvY3VtZW50CiAgIEZpbGU6IE9NQS1TVVAtTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci1WMV8wXzEtMjAxOTAyMjEtQQogICBUeXBlOiB4bWwKClB1YmxpYyBSZWFjaGFibGUgSW5mb3JtYXRpb24KICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMKICAgTmFtZTogTHdNMk1fQmluYXJ5QXBwRGF0YUNvbnRhaW5lci12MV8wXzEueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogIE9NQS1UUy1MV00yTV9CaW5hcnlBcHBEYXRhQ29udGFpbmVyLVYxXzBfMQoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvCgogIFNlbmQgY29tbWVudHMgdG8gaHR0cHM6Ly9naXRodWIuY29tL09wZW5Nb2JpbGVBbGxpYW5jZS9PTUFfTHdNMk1fZm9yX0RldmVsb3BlcnMvaXNzdWVzCgpDSEFOR0UgSElTVE9SWQoKMTUwNjIwMTggU3RhdHVzIGNoYW5nZWQgdG8gQXBwcm92ZWQgYnkgRE0sIERvYyBSZWYgIyBPTUEtRE0mU0UtMjAxOC0wMDYxLUlOUF9MV00yTV9BUFBEQVRBX1YxXzBfRVJQX2Zvcl9maW5hbF9BcHByb3ZhbAoyMTAyMjAxOSBTdGF0dXMgY2hhbmdlZCB0byBBcHByb3ZlZCBieSBJUFNPLCBEb2MgUmVmICMgT01BLUlQU08tMjAxOS0wMDI1LUlOUF9Md00yTV9PYmplY3RfQXBwX0RhdGFfQ29udGFpbmVyXzFfMF8xX2Zvcl9GaW5hbF9BcHByb3ZhbAoKTEVHQUwgRElTQ0xBSU1FUgoKQ29weXJpZ2h0IDIwMTkgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgpSZWRpc3RyaWJ1dGlvbiBhbmQgdXNlIGluIHNvdXJjZSBhbmQgYmluYXJ5IGZvcm1zLCB3aXRoIG9yIHdpdGhvdXQKbW9kaWZpY2F0aW9uLCBhcmUgcGVybWl0dGVkIHByb3ZpZGVkIHRoYXQgdGhlIGZvbGxvd2luZyBjb25kaXRpb25zCmFyZSBtZXQ6CgoxLiBSZWRpc3RyaWJ1dGlvbnMgb2Ygc291cmNlIGNvZGUgbXVzdCByZXRhaW4gdGhlIGFib3ZlIGNvcHlyaWdodApub3RpY2UsIHRoaXMgbGlzdCBvZiBjb25kaXRpb25zIGFuZCB0aGUgZm9sbG93aW5nIGRpc2NsYWltZXIuCjIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0Cm5vdGljZSwgdGhpcyBsaXN0IG9mIGNvbmRpdGlvbnMgYW5kIHRoZSBmb2xsb3dpbmcgZGlzY2xhaW1lciBpbiB0aGUKZG9jdW1lbnRhdGlvbiBhbmQvb3Igb3RoZXIgbWF0ZXJpYWxzIHByb3ZpZGVkIHdpdGggdGhlIGRpc3RyaWJ1dGlvbi4KMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwpjb250cmlidXRvcnMgbWF5IGJlIHVzZWQgdG8gZW5kb3JzZSBvciBwcm9tb3RlIHByb2R1Y3RzIGRlcml2ZWQKZnJvbSB0aGlzIHNvZnR3YXJlIHdpdGhvdXQgc3BlY2lmaWMgcHJpb3Igd3JpdHRlbiBwZXJtaXNzaW9uLgoKVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwoiQVMgSVMiIEFORCBBTlkgRVhQUkVTUyBPUiBJTVBMSUVEIFdBUlJBTlRJRVMsIElOQ0xVRElORywgQlVUIE5PVApMSU1JVEVEIFRPLCBUSEUgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSBBTkQgRklUTkVTUwpGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQpDT1BZUklHSFQgSE9MREVSIE9SIENPTlRSSUJVVE9SUyBCRSBMSUFCTEUgRk9SIEFOWSBESVJFQ1QsIElORElSRUNULApJTkNJREVOVEFMLCBTUEVDSUFMLCBFWEVNUExBUlksIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyAoSU5DTFVESU5HLApCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CkxPU1MgT0YgVVNFLCBEQVRBLCBPUiBQUk9GSVRTOyBPUiBCVVNJTkVTUyBJTlRFUlJVUFRJT04pIEhPV0VWRVIKQ0FVU0VEIEFORCBPTiBBTlkgVEhFT1JZIE9GIExJQUJJTElUWSwgV0hFVEhFUiBJTiBDT05UUkFDVCwgU1RSSUNUCkxJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCkFOWSBXQVkgT1VUIE9GIFRIRSBVU0UgT0YgVEhJUyBTT0ZUV0FSRSwgRVZFTiBJRiBBRFZJU0VEIE9GIFRIRQpQT1NTSUJJTElUWSBPRiBTVUNIIERBTUFHRS4KClRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiBQbGVhc2UKcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKaHR0cHM6Ly93d3cub21hc3BlY3dvcmtzLm9yZy9hYm91dC9pbnRlbGxlY3R1YWwtcHJvcGVydHktcmlnaHRzLwoKLS0+CjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL29wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS54c2QiPgoJPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgoJCTxOYW1lPkJpbmFyeUFwcERhdGFDb250YWluZXI8L05hbWU+CgkJPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdHMgcHJvdmlkZXMgdGhlIGFwcGxpY2F0aW9uIHNlcnZpY2UgZGF0YSByZWxhdGVkIHRvIGEgTHdNMk0gU2VydmVyLCBlZy4gV2F0ZXIgbWV0ZXIgZGF0YS4gClRoZXJlIGFyZSBzZXZlcmFsIG1ldGhvZHMgdG8gY3JlYXRlIGluc3RhbmNlIHRvIGluZGljYXRlIHRoZSBtZXNzYWdlIGRpcmVjdGlvbiBiYXNlZCBvbiB0aGUgbmVnb3RpYXRpb24gYmV0d2VlbiBBcHBsaWNhdGlvbiBhbmQgTHdNMk0uIFRoZSBDbGllbnQgYW5kIFNlcnZlciBzaG91bGQgbmVnb3RpYXRlIHRoZSBpbnN0YW5jZShzKSB1c2VkIHRvIGV4Y2hhbmdlIHRoZSBkYXRhLiBGb3IgZXhhbXBsZToKIC0gVXNpbmcgYSBzaW5nbGUgaW5zdGFuY2UgZm9yIGJvdGggZGlyZWN0aW9ucyBjb21tdW5pY2F0aW9uLCBmcm9tIENsaWVudCB0byBTZXJ2ZXIgYW5kIGZyb20gU2VydmVyIHRvIENsaWVudC4KIC0gVXNpbmcgYW4gaW5zdGFuY2UgZm9yIGNvbW11bmljYXRpb24gZnJvbSBDbGllbnQgdG8gU2VydmVyIGFuZCBhbm90aGVyIG9uZSBmb3IgY29tbXVuaWNhdGlvbiBmcm9tIFNlcnZlciB0byBDbGllbnQKIC0gVXNpbmcgc2V2ZXJhbCBpbnN0YW5jZXMKXV0+PC9EZXNjcmlwdGlvbjE+CgkJPE9iamVjdElEPjE5PC9PYmplY3RJRD4KCQk8T2JqZWN0VVJOPnVybjpvbWE6bHdtMm06b21hOjE5PC9PYmplY3RVUk4+CgkJPExXTTJNVmVyc2lvbj4xLjA8L0xXTTJNVmVyc2lvbj4KCQk8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CgkJPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJPFJlc291cmNlcz4KCQkJPEl0ZW0gSUQ9IjAiPjxOYW1lPkRhdGE8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+T3BhcXVlPC9UeXBlPgoJCQkJPFJhbmdlRW51bWVyYXRpb24gLz4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgYXBwbGljYXRpb24gZGF0YSBjb250ZW50Ll1dPjwvRGVzY3JpcHRpb24+CgkJCTwvSXRlbT4KCQkJPEl0ZW0gSUQ9IjEiPjxOYW1lPkRhdGEgUHJpb3JpdHk8L05hbWU+CgkJCQk8T3BlcmF0aW9ucz5SVzwvT3BlcmF0aW9ucz4KCQkJCTxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgoJCQkJPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgoJCQkJPFR5cGU+SW50ZWdlcjwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjEgYnl0ZXM8L1JhbmdlRW51bWVyYXRpb24+CgkJCQk8VW5pdHMgLz4KCQkJCTxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgdGhlIEFwcGxpY2F0aW9uIGRhdGEgcHJpb3JpdHk6CjA6SW1tZWRpYXRlCjE6QmVzdEVmZm9ydAoyOkxhdGVzdAozLTEwMDogUmVzZXJ2ZWQgZm9yIGZ1dHVyZSB1c2UuCjEwMS0yNTQ6IFByb3ByaWV0YXJ5IG1vZGUuXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iMiI+PE5hbWU+RGF0YSBDcmVhdGlvbiBUaW1lPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlRpbWU8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbiAvPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBEYXRhIGluc3RhbmNlIGNyZWF0aW9uIHRpbWVzdGFtcC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSIzIj48TmFtZT5EYXRhIERlc2NyaXB0aW9uPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPlN0cmluZzwvVHlwZT4KCQkJCTxSYW5nZUVudW1lcmF0aW9uPjMyIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkYXRhIGRlc2NyaXB0aW9uLgplLmcuICJtZXRlciByZWFkaW5nIi5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+CgkJCTxJdGVtIElEPSI0Ij48TmFtZT5EYXRhIEZvcm1hdDwvTmFtZT4KCQkJCTxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgoJCQkJPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CgkJCQk8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CgkJCQk8VHlwZT5TdHJpbmc8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4zMiBieXRlczwvUmFuZ2VFbnVtZXJhdGlvbj4KCQkJCTxVbml0cyAvPgoJCQkJPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgZm9ybWF0IG9mIHRoZSBBcHBsaWNhdGlvbiBEYXRhLgplLmcuIFlHLU1ldGVyLVdhdGVyLVJlYWRpbmcKVVRGOC1zdHJpbmcKXV0+PC9EZXNjcmlwdGlvbj4KCQkJPC9JdGVtPgoJCQk8SXRlbSBJRD0iNSI+PE5hbWU+QXBwIElEPC9OYW1lPgoJCQkJPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CgkJCQk8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KCQkJCTxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KCQkJCTxUeXBlPkludGVnZXI8L1R5cGU+CgkJCQk8UmFuZ2VFbnVtZXJhdGlvbj4yIGJ5dGVzPC9SYW5nZUVudW1lcmF0aW9uPgoJCQkJPFVuaXRzIC8+CgkJCQk8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHRoZSBkZXN0aW5hdGlvbiBBcHBsaWNhdGlvbiBJRC5dXT48L0Rlc2NyaXB0aW9uPgoJCQk8L0l0ZW0+PC9SZXNvdXJjZXM+CgkJPERlc2NyaXB0aW9uMj48IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMj4KCTwvT2JqZWN0Pgo8L0xXTTJNPgo="; private static final byte[] TEST_DATA = Base64.getDecoder().decode(TEST_BASE64_DATA); @@ -119,6 +131,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private ResourceService resourceService; @Autowired private TbResourceService tbResourceService; + @Autowired + private WidgetTypeService widgetTypeService; + @Autowired + private DashboardService dashboardService; private Tenant savedTenant; private User tenantAdmin; @@ -141,6 +157,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { tenantAdmin.setLastName("Downs"); tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } @After @@ -244,7 +261,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(title, foundResource.getTitle()); assertArrayEquals(foundResource.getData(), TEST_DATA); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test @@ -267,7 +284,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals("0_1.0", foundResource.getResourceKey()); assertArrayEquals(foundResource.getData(), LWM2M_TEST_MODEL.getBytes()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(savedResource, true, null); } @Test @@ -281,8 +298,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { assertEquals(TenantId.SYS_TENANT_ID, savedResource.getTenantId()); - TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(savedResource, true, null); } @Test @@ -361,7 +377,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(foundResource); assertEquals(savedResource, new TbResourceInfo(foundResource)); assertArrayEquals(TEST_DATA, foundResource.getData()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test @@ -378,22 +394,207 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(foundResource); assertEquals(savedResource, new TbResourceInfo(foundResource)); assertArrayEquals(TEST_DATA, foundResource.getData()); - tbResourceService.delete(foundResource, null); + tbResourceService.delete(foundResource, true, null); } @Test public void testDeleteResource() throws Exception { TbResource resource = new TbResource(); - resource.setResourceType(ResourceType.JKS); + resource.setResourceType(ResourceType.JS_MODULE); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData(TEST_DATA); TbResourceInfo savedResource = tbResourceService.save(resource); - TbResource foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + tbResourceService.delete(savedResource, true, null); + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNull(foundResource); + } + + @Test + public void testUnForceDeleteResourceAssignWithWidget() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(); + widgetTypeDetails.setTenantId(savedTenant.getId()); + widgetTypeDetails.setDescriptor(JacksonUtil.newObjectNode() + .put("sizeX", 3) + .put("sizeY", 3) + .put("resource", link) + .put("templateCss", "") + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config")); + widgetTypeDetails.setName("Widget Type"); + + WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(savedTenant.getId(), savedWidgetType.getId()); + String resourceLink = foundWidgetType.getDescriptor().get("resource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isSuccess()); + Assert.assertFalse(result.getReferences().isEmpty()); + Assert.assertEquals(1, result.getReferences().size()); + + WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); + WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType); + Assert.assertNotNull(widgetTypeInfo); + Assert.assertNotNull(foundWidgetTypeInfo); + Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo); + + TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedResource, foundResourceInfo); + } + + @Test + public void testForceDeleteResourceAssignWithWidget() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(); + widgetTypeDetails.setTenantId(savedTenant.getId()); + widgetTypeDetails.setDescriptor(JacksonUtil.newObjectNode() + .put("type", "rpc") + .put("sizeX", 3) + .put("sizeY", 3) + .put("resource", link) + .put("templateCss", "") + .put("controllerScript", "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}") + .put("settingsSchema", "") + .put("dataKeySettingsSchema", "{}\n") + .put("settingsDirective", "tb-scada-symbol-widget-settings") + .put("hasBasicMode", true) + .put("basicModeDirective", "tb-scada-symbol-basic-config")); + widgetTypeDetails.setName("Widget Type"); + + WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails); + WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(savedTenant.getId(), savedWidgetType.getId()); + String resourceLink = foundWidgetType.getDescriptor().get("resource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, true, null); + Assert.assertNotNull(result); + Assert.assertTrue(result.isSuccess()); + Assert.assertNull(result.getReferences()); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNull(foundResource); + } + + @Test + public void testUnForceDeleteResourceAssignWithDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .put("widgets", """ + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """) + .put("someResource", link)); + + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); + Dashboard foundDashboard = dashboardService.findDashboardById(savedTenant.getId(), savedDashboard.getId()); + String resourceLink = foundDashboard.getConfiguration().get("someResource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isSuccess()); + Assert.assertNotNull(result.getReferences()); + Assert.assertEquals(1, result.getReferences().size()); + + DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); + DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId()); + Assert.assertNotNull(dashboardInfo); + Assert.assertNotNull(foundDashboardInfo); + Assert.assertEquals(foundDashboardInfo, dashboardInfo); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + Assert.assertNotNull(foundResource); + Assert.assertEquals(savedDashboard, foundDashboard); + } + + @Test + public void testForceDeleteResourceAssignWithDashboard() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My resource"); + resource.setFileName(JS_FILE_NAME); + resource.setTenantId(savedTenant.getId()); + resource.setData(TEST_DATA); + TbResourceInfo savedResource = tbResourceService.save(resource); + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); - tbResourceService.delete(foundResource, null); - foundResource = resourceService.findResourceById(tenantId, savedResource.getId()); + String link = DataConstants.TB_RESOURCE_PREFIX + resource.getLink(); + + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("My dashboard"); + dashboard.setTenantId(savedTenant.getId()); + dashboard.setConfiguration(JacksonUtil.newObjectNode() + .set("widgets", JacksonUtil.toJsonNode(""" + {"xxx": + {"config":{"actions":{"elementClick":[ + {"customResources":[{"url":{"entityType":"TB_RESOURCE","id": + "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js"},"isModule":true}, + {"url":"tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js","isModule":true}]}]}}}} + """)) + .put("someResource", link)); + + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); + Dashboard foundDashboard = dashboardService.findDashboardById(savedTenant.getId(), savedDashboard.getId()); + String resourceLink = foundDashboard.getConfiguration().get("someResource").asText(); + Assertions.assertNotNull(resourceLink); + Assert.assertEquals(resourceLink, link); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, true, null); + Assert.assertNotNull(result); + Assert.assertTrue(result.isSuccess()); + Assert.assertNull(result.getReferences()); + + foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNull(foundResource); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index d6074ca48b..ee187db46b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.TbResourceId; @@ -69,9 +70,7 @@ public interface ResourceService extends EntityDaoService { PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink); - void deleteResource(TenantId tenantId, TbResourceId resourceId); - - void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); + TbResourceDeleteResult deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); void deleteResourcesByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java new file mode 100644 index 0000000000..edc5a2f539 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 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; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.HasId; + +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class TbResourceDeleteResult { + + private boolean success; + private Map>> references; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java new file mode 100644 index 0000000000..6a952fd501 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 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; + +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.List; + +public interface ResourceContainerDao> { + + List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); + + List findByResourceLink(String link, int limit); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index 088b9e8a1f..6a3ec3295e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ImageContainerDao; +import org.thingsboard.server.dao.ResourceContainerDao; import java.util.UUID; /** * The Interface DashboardInfoDao. */ -public interface DashboardInfoDao extends Dao, ImageContainerDao { +public interface DashboardInfoDao extends Dao, ImageContainerDao, ResourceContainerDao { /** * Find dashboards by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index f8fe55cf02..e91c1939f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -75,8 +75,6 @@ import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; @Slf4j public class BaseImageService extends BaseResourceService implements ImageService { - private static final int MAX_ENTITIES_TO_FIND = 10; - public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); @@ -107,19 +105,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic private final AssetProfileDao assetProfileDao; private final DeviceProfileDao deviceProfileDao; private final WidgetsBundleDao widgetsBundleDao; - private final WidgetTypeDao widgetTypeDao; - private final DashboardInfoDao dashboardInfoDao; private final Map> imageContainerDaoMap = new HashMap<>(); public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { - super(resourceDao, resourceInfoDao, resourceValidator); + super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); this.assetProfileDao = assetProfileDao; this.deviceProfileDao = deviceProfileDao; this.widgetsBundleDao = widgetsBundleDao; - this.widgetTypeDao = widgetTypeDao; - this.dashboardInfoDao = dashboardInfoDao; } @PostConstruct @@ -131,7 +125,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic imageContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); } - @Override @SneakyThrows public TbResourceInfo saveImage(TbResource image) { @@ -311,7 +304,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic } } if (success) { - deleteResource(tenantId, imageId, force); + success = deleteResource(tenantId, imageId, true) + .isSuccess(); } return result.success(success).build(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 76aefdbb93..bf73c59708 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.id.EntityId; @@ -48,6 +50,8 @@ 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.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.ResourceContainerDao; +import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -55,6 +59,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; +import org.thingsboard.server.dao.widget.WidgetTypeDao; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -85,6 +90,17 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>(); + protected static final int MAX_ENTITIES_TO_FIND = 10; + + @PostConstruct + public void init() { + resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + } + @Autowired @Lazy private ImageService imageService; @@ -313,23 +329,45 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); TbResourceInfo resource = findResourceInfoById(tenantId, resourceId); + boolean success = true; + var result = TbResourceDeleteResult.builder(); + if (resource == null) { - return; + if (!force) { + success = false; + } + return result.success(success).build(); } + if (!force) { - resourceValidator.validateDelete(tenantId, resource); + if (resource.getResourceType() == ResourceType.JS_MODULE) { + var link = resource.getLink(); + Map>> affectedEntities = new HashMap<>(); + + resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> { + var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) : + resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + affectedEntities.put(entityType.name(), entities); + } + }); + + if (!affectedEntities.isEmpty()) { + success = false; + result.references(affectedEntities); + } + } } - resourceDao.removeById(tenantId, resourceId.getId()); - eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); + if (success) { + resourceDao.removeById(tenantId, resourceId.getId()); + eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); + } + + return result.success(success).build(); } @Override @@ -666,7 +704,7 @@ public class BaseResourceService extends AbstractCachedEntityService { @Autowired private TbResourceDao resourceDao; - @Autowired - private WidgetTypeDao widgetTypeDao; - @Autowired private TenantService tenantService; @@ -111,12 +104,4 @@ public class ResourceDataValidator extends DataValidator { validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE); } } - - public void validateDelete(TenantId tenantId, TbResourceInfo resourceInfo) { - List widgets = widgetTypeDao.findWidgetTypesNamesByTenantIdAndResourceLink(tenantId.getId(), resourceInfo.getLink()); - if (!widgets.isEmpty()) { - throw new DataValidationException("Following widget types use this resource: " + String.join(", ", widgets)); - } - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index cddbe3e347..7624ddc738 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -78,13 +78,21 @@ public interface DashboardInfoRepository extends JpaRepository findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit); @Query(nativeQuery = true, - value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt" + value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :limit" ) - List findByImageLink(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit); + + @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", + nativeQuery = true) + List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + + @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", + nativeQuery = true) + List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 249e9d6e5b..bc07139725 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -133,4 +133,15 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByImageLink(String imageLink, int limit) { return DaoUtil.convertDataList(dashboardInfoRepository.findByImageLink(imageLink, limit)); } + + @Override + public List findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit)); + } + + @Override + public List findByResourceLink(String link, int limit) { + return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 5d207800d7..df10804d50 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -190,11 +190,6 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link) { - return widgetTypeRepository.findNamesByTenantIdAndResourceLink(tenantId, link); - } - @Override public List findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns) { var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns); @@ -266,4 +261,14 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit)); + } + + @Override + public List findByResourceLink(String link, int limit) { + return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit)); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index 39ba949ca9..dc79280bcf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -204,13 +204,20 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByTenantAndImageUrl(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("limit") int limit); @Query(nativeQuery = true, value = "SELECT * FROM widget_type_info_view wti WHERE wti.id IN " + - "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :lmt)" + "(select id from widget_type where image = :imageLink or descriptor ILIKE CONCAT('%', :imageLink, '%') limit :limit)" ) - List findByImageUrl(@Param("imageLink") String imageLink, @Param("lmt") int lmt); + List findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit); + + @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) + List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + + @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) + List findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index d92bd085cf..e148c10d74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -69,12 +69,6 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE concat('%', :resourceLink, '%')", - nativeQuery = true) - List findNamesByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, - @Param("resourceLink") String resourceLink); - @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java index 7302ed41bc..bbb8d026c0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ImageContainerDao; +import org.thingsboard.server.dao.ResourceContainerDao; import java.util.List; import java.util.UUID; @@ -35,7 +36,7 @@ import java.util.UUID; /** * The Interface WidgetTypeDao. */ -public interface WidgetTypeDao extends Dao, ExportableEntityDao, ImageContainerDao { +public interface WidgetTypeDao extends Dao, ExportableEntityDao, ImageContainerDao, ResourceContainerDao { /** * Save or update widget type object @@ -97,8 +98,6 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD WidgetTypeDetails findDetailsByTenantIdAndFqn(UUID tenantId, String fqn); - List findWidgetTypesNamesByTenantIdAndResourceLink(UUID tenantId, String link); - List findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List widgetFqns); List findWidgetsBundleWidgetsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 9f0daa73ee..615b721b97 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -90,8 +90,8 @@ export class ResourceService { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } - public deleteResource(resourceId: string, config?: RequestConfig) { - return this.http.delete(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config)); + public deleteResource(resourceId: string, force = false, config?: RequestConfig) { + return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index 314ae7809f..9c5dba6dbb 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { + CellActionDescriptor, checkBoxCell, DateEntityTableColumn, EntityTableColumn, @@ -27,7 +28,9 @@ import { ResourceInfo, ResourceSubType, ResourceSubTypeTranslationMap, - ResourceType + ResourceType, + ResourceInfoWithReferences, + toResourceDeleteResult } from '@shared/models/resource.models'; import { EntityType, entityTypeResources } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; @@ -42,8 +45,19 @@ import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; -import { switchMap } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; +import { forkJoin, of } from 'rxjs'; +import { parseHttpErrorMessage } from '@core/utils'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { + ResourcesInUseDialogComponent, + ResourcesInUseDialogData +} from "@shared/components/resource/resources-in-use-dialog.component"; +import { ResourcesDatasource } from "@home/pages/admin/resource/resources-datasource"; +import { AuthUser } from '@shared/models/user.model'; @Injectable() export class JsLibraryTableConfigResolver { @@ -53,6 +67,8 @@ export class JsLibraryTableConfigResolver { constructor(private store: Store, private resourceService: ResourceService, private translate: TranslateService, + private dialog: MatDialog, + private dialogService: DialogService, private router: Router, private datePipe: DatePipe) { @@ -81,20 +97,16 @@ export class JsLibraryTableConfigResolver { entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), ); - this.config.cellActionDescriptors.push( - { - name: this.translate.instant('javascript.download'), - icon: 'file_download', - isEnabled: () => true, - onAction: ($event, entity) => this.downloadResource($event, entity) - } - ); + this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store)); - this.config.deleteEntityTitle = resource => this.translate.instant('javascript.delete-javascript-resource-title', - { resourceTitle: resource.title }); - this.config.deleteEntityContent = () => this.translate.instant('javascript.delete-javascript-resource-text'); - this.config.deleteEntitiesTitle = count => this.translate.instant('javascript.delete-javascript-resources-title', {count}); - this.config.deleteEntitiesContent = () => this.translate.instant('javascript.delete-javascript-resources-text'); + this.config.groupActionDescriptors = [{ + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteResources($event, entities) + }]; + + this.config.entitiesDeleteEnabled = false; this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, ResourceType.JS_MODULE, this.config.componentsData.resourceSubType); this.config.loadEntity = id => { @@ -115,7 +127,6 @@ export class JsLibraryTableConfigResolver { } return saveObservable; }; - this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.onEntityAction = action => this.onResourceAction(action); } @@ -126,7 +137,6 @@ export class JsLibraryTableConfigResolver { resourceSubType: '' }; const authUser = getCurrentAuthUser(this.store); - this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); return this.config; @@ -170,4 +180,151 @@ export class JsLibraryTableConfigResolver { return authority === Authority.SYS_ADMIN; } } + + private deleteResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('javascript.delete-javascript-resource-title', { resourceTitle: resource.title }), + this.translate.instant('javascript.delete-javascript-resource-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ).subscribe( + (deleteResult) => { + if (deleteResult.success) { + this.config.updateData(); + } else if (deleteResult.resourceIsReferencedError) { + const resources: ResourceInfoWithReferences[] = [{...resource, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources, + configuration: { + title: 'javascript.javascript-resource-is-in-use', + message: this.translate.instant('javascript.javascript-resource-is-in-use-text', {title: resources[0].title}), + deleteText: 'javascript.delete-javascript-resource-in-use-text', + selectedText: 'javascript.selected-javascript-resources', + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((resources) => { + if (resources) { + this.resourceService.deleteResource(resource.id.id, true).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else { + const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + + private deleteResources($event: Event, resources: ResourceInfo[]) { + if ($event) { + $event.stopPropagation(); + } + if (resources && resources.length) { + const title = this.translate.instant('javascript.delete-javascript-resources-title', {count: resources.length}); + const content = this.translate.instant('javascript.delete-javascript-resources-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + const tasks = resources.map((resource) => + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ) + ); + forkJoin(tasks).subscribe( + (deleteResults) => { + const anySuccess = deleteResults.some(res => res.success); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); + const otherError = deleteResults.find(res => !res.success); + if (anySuccess) { + this.config.updateData(); + } + if (referenceErrors?.length) { + const resourcesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: resourcesWithReferences, + configuration: { + title: 'javascript.javascript-resources-are-in-use', + message: this.translate.instant('javascript.javascript-resources-are-in-use-text'), + deleteText: 'javascript.delete-javascript-resource-in-use-text', + selectedText: 'javascript.selected-javascript-resources', + datasource: new ResourcesDatasource(this.resourceService, resourcesWithReferences, entity => true), + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((forceDeleteResources) => { + if (forceDeleteResources && forceDeleteResources.length) { + const forceDeleteTasks = forceDeleteResources.map((resource) => + this.resourceService.deleteResource(resource.id.id, true) + ); + forkJoin(forceDeleteTasks).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else if (otherError) { + const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + } + + private configureCellActions(authUser: AuthUser): Array> { + const actions: Array> = []; + actions.push( + { + name: this.translate.instant('javascript.download'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.downloadResource($event, entity) + }, + { + name: this.translate.instant('javascript.delete'), + icon: 'delete', + isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority), + onAction: ($event, entity) => this.deleteResource($event, entity) + }, + ); + return actions; + } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts new file mode 100644 index 0000000000..ed9c9f5dd9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-datasource.ts @@ -0,0 +1,130 @@ +/// +/// Copyright © 2016-2025 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. +/// + +import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; +import { ResourceInfo, ResourceSubType, ResourceType } from '@shared/models/resource.models'; +import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { EntityBooleanFunction } from '@home/models/entity/entities-table-config.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, take, tap } from 'rxjs/operators'; +import { ResourceService } from "@core/http/resource.service"; + +export class ResourcesDatasource implements DataSource { + private entitiesSubject: Subject; + private readonly pageDataSubject: Subject>; + + public pageData$: Observable>; + + public selection = new SelectionModel(true, []); + + public dataLoading = true; + + constructor(private resourceService: ResourceService, + private resources: ResourceInfo[], + private selectionEnabledFunction: EntityBooleanFunction) { + if (this.resources && this.resources.length) { + this.entitiesSubject = new BehaviorSubject(this.resources); + } else { + this.entitiesSubject = new BehaviorSubject([]); + this.pageDataSubject = new BehaviorSubject>(emptyPageData()); + this.pageData$ = this.pageDataSubject.asObservable(); + } + } + + connect(collectionViewer: CollectionViewer): + Observable> { + return this.entitiesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.entitiesSubject.complete(); + if (this.pageDataSubject) { + this.pageDataSubject.complete(); + } + } + + reset() { + this.entitiesSubject.next([]); + if (this.pageDataSubject) { + this.pageDataSubject.next(emptyPageData()); + } + } + + loadEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable> { + this.dataLoading = true; + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, resourceType, subType).pipe( + tap(() => { + this.selection.clear(); + }), + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.entitiesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, resourceType: ResourceType, subType: ResourceSubType): Observable> { + return this.resourceService.getResources(pageLink, resourceType, subType); + } + + isAllSelected(): Observable { + const numSelected = this.selection.selected.length; + return this.entitiesSubject.pipe( + map((entities) => numSelected === entities.length) + ); + } + + isEmpty(): Observable { + return this.entitiesSubject.pipe( + map((entities) => !entities.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + + masterToggle() { + this.entitiesSubject.pipe( + tap((entities) => { + const numSelected = this.selection.selected.length; + if (numSelected === this.selectableEntitiesCount(entities)) { + this.selection.clear(); + } else { + entities.forEach(row => { + if (this.selectionEnabledFunction(row)) { + this.selection.select(row); + } + }); + } + }), + take(1) + ).subscribe(); + } + + private selectableEntitiesCount(entities: Array): number { + return entities.filter((entity) => this.selectionEnabledFunction(entity)).length; + } +} diff --git a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts index ef22c037f2..6e40683c2b 100644 --- a/ui-ngx/src/app/shared/components/image/image-gallery.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-gallery.component.ts @@ -16,10 +16,10 @@ import { ImageResourceInfo, - ImageResourceInfoWithReferences, + ResourceInfoWithReferences, imageResourceType, ResourceSubType, - toImageDeleteResult + toResourceDeleteResult } from '@shared/models/resource.models'; import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { ImageService } from '@core/http/image.service'; @@ -67,9 +67,9 @@ import { ImageDialogComponent, ImageDialogData } from '@shared/components/image/ import { ImportExportService } from '@shared/import-export/import-export.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { - ImagesInUseDialogComponent, - ImagesInUseDialogData -} from '@shared/components/image/images-in-use-dialog.component'; + ResourcesInUseDialogComponent, + ResourcesInUseDialogData +} from '@shared/components/resource/resources-in-use-dialog.component'; import { ImagesDatasource } from '@shared/components/image/images-datasource'; import { EmbedImageDialogComponent, EmbedImageDialogData } from '@shared/components/image/embed-image-dialog.component'; @@ -504,21 +504,30 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe this.translate.instant('action.yes')).subscribe((result) => { if (result) { this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( - map(() => toImageDeleteResult(image)), - catchError((err) => of(toImageDeleteResult(image, err))) + map(() => toResourceDeleteResult(image)), + catchError((err) => of(toResourceDeleteResult(image, err))) ).subscribe( (deleteResult) => { if (deleteResult.success) { this.imageDeleted(itemIndex); - } else if (deleteResult.imageIsReferencedError) { - this.dialog.open(ImagesInUseDialogComponent, { + } else if (deleteResult.resourceIsReferencedError) { + const images = [{...image, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources: images, + configuration: { + title: 'image.image-is-in-use', + message: this.translate.instant('image.image-is-in-use-text', {title: images[0].title}), + deleteText: 'image.delete-image-in-use-text', + selectedText: 'image.selected-images', + columns: ['select', 'preview', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - multiple: false, - images: [{...image, ...{references: deleteResult.references}}] - } + data }).afterClosed().subscribe((images) => { if (images) { this.imageService.deleteImage(imageResourceType(image), image.resourceKey, true).subscribe( @@ -554,29 +563,38 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe if (result) { const tasks = selectedImages.map((image) => this.imageService.deleteImage(imageResourceType(image), image.resourceKey, false, {ignoreErrors: true}).pipe( - map(() => toImageDeleteResult(image)), - catchError((err) => of(toImageDeleteResult(image, err))) + map(() => toResourceDeleteResult(image)), + catchError((err) => of(toResourceDeleteResult(image, err))) ) ); forkJoin(tasks).subscribe( (deleteResults) => { const anySuccess = deleteResults.some(res => res.success); - const referenceErrors = deleteResults.filter(res => res.imageIsReferencedError); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); const otherError = deleteResults.find(res => !res.success); if (anySuccess) { this.updateData(); } if (referenceErrors?.length) { - const imagesWithReferences: ImageResourceInfoWithReferences[] = - referenceErrors.map(ref => ({...ref.image, ...{references: ref.references}})); - this.dialog.open(ImagesInUseDialogComponent, { + const imagesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: imagesWithReferences, + configuration: { + title: 'image.images-are-in-use', + message: this.translate.instant('image.images-are-in-use-text'), + deleteText: 'image.delete-image-in-use-text', + selectedText: 'image.selected-images', + columns: ['select', 'preview', 'title', 'references'], + datasource: new ImagesDatasource(null, imagesWithReferences, entity => true) + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - multiple: true, - images: imagesWithReferences - } + data }).afterClosed().subscribe((forceDeleteImages) => { if (forceDeleteImages && forceDeleteImages.length) { const forceDeleteTasks = forceDeleteImages.map((image) => diff --git a/ui-ngx/src/app/shared/components/image/image-references.component.ts b/ui-ngx/src/app/shared/components/image/image-references.component.ts index af2b4e9798..802408ee3a 100644 --- a/ui-ngx/src/app/shared/components/image/image-references.component.ts +++ b/ui-ngx/src/app/shared/components/image/image-references.component.ts @@ -17,7 +17,7 @@ import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { ImageReferences } from '@shared/models/resource.models'; +import { ResourceReferences } from '@shared/models/resource.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { getEntityDetailsPageURL } from '@core/utils'; @@ -54,7 +54,7 @@ type ReferencedEntitiesEntry = [string, TenantReferencedEntities]; export class ImageReferencesComponent implements OnInit { @Input() - references: ImageReferences; + references: ResourceReferences; popoverComponent: TbPopoverComponent; @@ -99,7 +99,7 @@ export class ImageReferencesComponent implements OnInit { return tenantId === NULL_UUID; } - private hasNonSystemEntities(references: ImageReferences): boolean { + private hasNonSystemEntities(references: ResourceReferences): boolean { for (const entityTypeStr of Object.keys(references)) { const entities = this.references[entityTypeStr]; if (entities.some(e => e.tenantId && e.tenantId.id && e.tenantId.id !== NULL_UUID)) { @@ -109,7 +109,7 @@ export class ImageReferencesComponent implements OnInit { return false; } - private toReferencedEntitiesList(references: ImageReferences): ReferencedEntityInfo[] { + private toReferencedEntitiesList(references: ResourceReferences): ReferencedEntityInfo[] { const result: ReferencedEntityInfo[] = []; for (const entityTypeStr of Object.keys(references)) { const entityType = entityTypeStr as EntityType; @@ -127,7 +127,7 @@ export class ImageReferencesComponent implements OnInit { return result; } - private toReferencedEntitiesEntries(references: ImageReferences): Observable { + private toReferencedEntitiesEntries(references: ResourceReferences): Observable { let referencedEntities: ReferencedEntities = {}; const referencedEntitiesList = this.toReferencedEntitiesList(references); for (const referencedEntityInfo of referencedEntitiesList) { diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html similarity index 71% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html index 4f4667ceb8..08a35aa412 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.html +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.html @@ -15,10 +15,10 @@ limitations under the License. --> -

{{title}}

-
-
- +

{{configuration.title}}

+
+
+
@@ -28,14 +28,14 @@ [indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false"> - + + (change)="$event ? dataSource.selection.toggle(resource) : null" + [checked]="dataSource.selection.isSelected(resource)"> - + {{ image.title }} @@ -43,30 +43,30 @@ - - {{ 'image.name' | translate }} + + {{ 'resource.title' | translate }} - - {{ translate.get('image.selected-images', {count: dataSource.selection.selected.length}) | async }} + + {{ translate.get(configuration.selectedText, {count: dataSource.selection.selected.length}) | async }} - - {{ image.title }} + + {{ resource.title }} - + + (click)="toggleShowReferences($event, resource, showReferencesButton)">{{ 'image.references' | translate }} - + + [class.mat-selected]="dataSource.selection.isSelected(resource)" + *matRowDef="let resource; columns: configuration.columns;">
@@ -76,7 +76,7 @@
- + -
+
diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss similarity index 96% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss index 5c053e984f..380a6070eb 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.scss +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.scss @@ -14,7 +14,7 @@ * limitations under the License. */ :host { - .tb-images-in-use-content { + .tb-resources-in-use-content { display: flex; flex-direction: column; gap: 24px; diff --git a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts similarity index 66% rename from ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts rename to ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts index 681e9a2c53..6f83fded7d 100644 --- a/ui-ngx/src/app/shared/components/image/images-in-use-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/resource/resources-in-use-dialog.component.ts @@ -20,37 +20,50 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; -import { ImageReferences, ImageResourceInfo, ImageResourceInfoWithReferences } from '@shared/models/resource.models'; -import { ImagesDatasource } from '@shared/components/image/images-datasource'; +import { + ResourceReferences, + ResourceInfoWithReferences, + ResourceInfo +} from '@shared/models/resource.models'; import { MatButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; import { TranslateService } from '@ngx-translate/core'; +import { Datasource } from "@shared/models/widget.models"; -export interface ImagesInUseDialogData { +interface ResourcesInUseDialogDataConfiguration { + title: string; + message: string; + columns: string[]; + deleteText: string; + selectedText: string; + datasource?: Datasource; +} + +export interface ResourcesInUseDialogData { multiple: boolean; - images: ImageResourceInfoWithReferences[]; + resources: ResourceInfoWithReferences[]; + configuration: ResourcesInUseDialogDataConfiguration; } @Component({ - selector: 'tb-images-in-use-dialog', - templateUrl: './images-in-use-dialog.component.html', - styleUrls: ['./images-in-use-dialog.component.scss'] + selector: 'tb-resources-in-use-dialog', + templateUrl: './resources-in-use-dialog.component.html', + styleUrls: ['./resources-in-use-dialog.component.scss'] }) -export class ImagesInUseDialogComponent extends - DialogComponent implements OnInit { - - title: string; - message: string; +export class ResourcesInUseDialogComponent extends + DialogComponent implements OnInit { - references: ImageReferences; + displayPreview: boolean; + configuration: ResourcesInUseDialogDataConfiguration; + references: ResourceReferences; - dataSource: ImagesDatasource; + dataSource: Datasource; constructor(protected store: Store, protected router: Router, - @Inject(MAT_DIALOG_DATA) public data: ImagesInUseDialogData, - public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ResourcesInUseDialogData, + public dialogRef: MatDialogRef, public translate: TranslateService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, @@ -59,14 +72,12 @@ export class ImagesInUseDialogComponent extends } ngOnInit(): void { + this.configuration = this.data.configuration; + this.displayPreview = this.data.configuration.columns.includes('preview'); if (this.data.multiple) { - this.title = this.translate.instant('image.images-are-in-use'); - this.message = this.translate.instant('image.images-are-in-use-text'); - this.dataSource = new ImagesDatasource(null, this.data.images, entity => true); + this.dataSource = this.data.configuration.datasource; } else { - this.title = this.translate.instant('image.image-is-in-use'); - this.message = this.translate.instant('image.image-is-in-use-text', {title: this.data.images[0].title}); - this.references = this.data.images[0].references; + this.references = this.data.resources[0].references; } } @@ -78,11 +89,11 @@ export class ImagesInUseDialogComponent extends if (this.data.multiple) { this.dialogRef.close(this.dataSource.selection.selected); } else { - this.dialogRef.close(this.data.images); + this.dialogRef.close(this.data.resources); } } - toggleShowReferences($event: Event, image: ImageResourceInfoWithReferences, referencesButton: MatButton) { + toggleShowReferences($event: Event, resource: ResourceInfoWithReferences, referencesButton: MatButton) { if ($event) { $event.stopPropagation(); } @@ -93,7 +104,7 @@ export class ImagesInUseDialogComponent extends const referencesPopover = this.popoverService.displayPopover(trigger, this.renderer, this.viewContainerRef, ImageReferencesComponent, 'top', true, null, { - references: image.references + references: resource.references }, {}, {}, {}, false, visible => { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index eda0182383..0419e40a7c 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -120,28 +120,28 @@ export interface ImageExportData { export type ImageResourceType = 'tenant' | 'system'; export type TBResourceScope = 'tenant' | 'system'; -export type ImageReferences = {[entityType: string]: Array & HasTenantId>}; +export type ResourceReferences = {[entityType: string]: Array & HasTenantId>}; -export interface ImageResourceInfoWithReferences extends ImageResourceInfo { - references: ImageReferences; +export interface ResourceInfoWithReferences extends ResourceInfo { + references: ResourceReferences; } -export interface ImageDeleteResult { - image: ImageResourceInfo; +export interface ResourceDeleteResult { + resource: TbResourceInfo; success: boolean; - imageIsReferencedError?: boolean; + resourceIsReferencedError?: boolean; error?: any; - references?: ImageReferences; + references?: ResourceReferences; } -export const toImageDeleteResult = (image: ImageResourceInfo, e?: any): ImageDeleteResult => { +export const toResourceDeleteResult = (resource: ResourceInfo, e?: any): ResourceDeleteResult => { if (!e) { - return {image, success: true}; + return {resource, success: true}; } else { - const result: ImageDeleteResult = {image, success: false, error: e}; + const result: ResourceDeleteResult = {resource, success: false, error: e}; if (e?.status === 400 && e?.error?.success === false && e?.error?.references) { - const references: ImageReferences = e?.error?.references; - result.imageIsReferencedError = true; + const references: ResourceReferences = e?.error?.references; + result.resourceIsReferencedError = true; result.references = references; } return result; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9245673d89..9b02d49c8f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -201,7 +201,7 @@ import { ImageGalleryComponent } from '@shared/components/image/image-gallery.co import { UploadImageDialogComponent } from '@shared/components/image/upload-image-dialog.component'; import { ImageDialogComponent } from '@shared/components/image/image-dialog.component'; import { ImageReferencesComponent } from '@shared/components/image/image-references.component'; -import { ImagesInUseDialogComponent } from '@shared/components/image/images-in-use-dialog.component'; +import { ResourcesInUseDialogComponent } from '@shared/components/resource/resources-in-use-dialog.component'; import { GalleryImageInputComponent } from '@shared/components/image/gallery-image-input.component'; import { MultipleGalleryImageInputComponent } from '@shared/components/image/multiple-gallery-image-input.component'; import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component'; @@ -425,7 +425,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) UploadImageDialogComponent, ImageDialogComponent, ImageReferencesComponent, - ImagesInUseDialogComponent, + ResourcesInUseDialogComponent, GalleryImageInputComponent, MultipleGalleryImageInputComponent, EmbedImageDialogComponent, @@ -688,7 +688,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) UploadImageDialogComponent, ImageDialogComponent, ImageReferencesComponent, - ImagesInUseDialogComponent, + ResourcesInUseDialogComponent, GalleryImageInputComponent, MultipleGalleryImageInputComponent, EmbedImageDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 18801bda2c..de97184f5a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4265,6 +4265,7 @@ "delete-javascript-resources-action-title": "Delete JavaScript { count, plural, =1 {1 resource} other {# resources} }", "delete-javascript-resources-text": "Please note that the selected JavaScript resources, even if they are used in JavaScript functions, will be deleted.", "delete-javascript-resources-title": "Are you sure you want to delete JavaScript { count, plural, =1 {1 resource} other {# resources} }?", + "delete-javascript-resource-in-use-text": "If you still want to delete the JavaScript resource, click the Delete anyway button.", "download": "Download JavaScript resource", "upload-from-file": "Upload JavaScript from file", "resource-file": "JavaScript resource file", @@ -4273,6 +4274,10 @@ "javascript-library": "JavaScript library", "javascript-type": "JavaScript type", "javascript-resource-details": "JavaScript resource details", + "javascript-resource-is-in-use": "JavaScript resource is used by other entities", + "javascript-resources-are-in-use": "JavaScript resources are used by other entities", + "javascript-resource-is-in-use-text": "The JavaScript resource '{{title}}' was not deleted because it is used by the following entities:", + "javascript-resources-are-in-use-text": "Not all JavaScript resources have been deleted because they are used by other entities.
You can view referenced entities by clicking the References button in the corresponding resource row.
If you still want to delete these JavaScript resources, select them in the table below and click the Delete selected button.", "search": "Search JavaScript resources", "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript resource} other {# JavaScript resources} } selected", "no-javascript-resource-text": "No JavaScript resources found", From e78fe241a01c89ae28b07721e45e5878e715761b Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 11:46:43 +0200 Subject: [PATCH 29/42] Added tenant profile configs for Calculated Fields --- ui-ngx/src/app/core/auth/auth.models.ts | 2 + ui-ngx/src/app/core/auth/auth.reducer.ts | 2 + ...lated-field-arguments-table.component.html | 17 +++- ...lated-field-arguments-table.component.scss | 6 ++ ...culated-field-arguments-table.component.ts | 5 + ...ulated-field-argument-panel.component.html | 17 +++- ...ulated-field-argument-panel.component.scss | 4 +- ...lculated-field-argument-panel.component.ts | 11 ++- ...enant-profile-configuration.component.html | 92 +++++++++++++++++++ ...-tenant-profile-configuration.component.ts | 8 +- .../tenant/rate-limits/rate-limits.models.ts | 5 +- .../assets/locale/locale.constant-en_US.json | 21 ++++- 12 files changed, 175 insertions(+), 15 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 55af791156..1944c693ac 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -28,6 +28,8 @@ export interface SysParamsState { userSettings: UserSettings; maxResourceSize: number; maxDebugModeDurationMinutes: number; + maxDataPointsPerRollingArg: number; + maxArgumentsPerCF: number; ruleChainDebugPerTenantLimitsConfiguration?: string; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index a460cf35bb..3ecf70074c 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -31,6 +31,8 @@ const emptyUserAuthState: AuthPayload = { persistDeviceStateToTelemetry: false, mobileQrEnabled: false, maxResourceSize: 0, + maxArgumentsPerCF: 0, + maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, userSettings: initialUserSettings }; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 96dff1c1be..5510aeb2c2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -119,9 +119,22 @@ }
-
- + @if (!!maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF) { +
+ warning + {{ 'calculated-fields.hint.max-args' | translate }} +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index c4efe323c6..877a749afa 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -30,6 +30,12 @@ } } + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + .tb-form-table-row-cell-buttons { --mat-badge-legacy-small-size-container-size: 8px; --mat-badge-small-size-container-overlap-offset: -5px; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 2537f34059..e3b19503af 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -55,6 +55,9 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { EntityService } from '@core/http/entity.service'; import { MatSort } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; @Component({ selector: 'tb-calculated-field-arguments-table', @@ -93,6 +96,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces readonly ArgumentEntityType = ArgumentEntityType; readonly ArgumentType = ArgumentType; readonly CalculatedFieldType = CalculatedFieldType; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF; private popoverComponent: TbPopoverComponent; private propagateChange: (argumentsObj: Record) => void = () => {}; @@ -105,6 +109,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces private renderer: Renderer2, private entityService: EntityService, private destroyRef: DestroyRef, + private store: Store ) { this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { this.updateEntityNameMap(value); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 72bc85a48f..566a433545 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -166,10 +166,19 @@ formControlName="timeWindow" /> -
-
{{ 'calculated-fields.limit' | translate }}
- -
+ @if (maxDataPointsPerRollingArg) { +
+
{{ 'calculated-fields.limit' | translate }}
+
+ + + + + + +
+
+ } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss index 1f7c084d08..773489ee60 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss @@ -28,9 +28,7 @@ $panel-width: 520px; min-width: 120px; } } -} -:host ::ng-deep { .limit-field-row { @media screen and (max-width: $panel-width) { display: flex; @@ -42,7 +40,9 @@ $panel-width: 520px; } } } +} +:host ::ng-deep { .time-interval-field { .advanced-input { flex-direction: column; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 93b8f897ce..9d9614ba28 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -38,7 +38,9 @@ import { EntityFilter } from '@shared/models/query/query.models'; import { AliasFilterType } from '@shared/models/alias.models'; import { merge } from 'rxjs'; import { MINUTE } from '@shared/models/time/time.models'; -import { TimeService } from '@core/services/time.service'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; @Component({ selector: 'tb-calculated-field-argument-panel', @@ -58,7 +60,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>(); - readonly defaultLimit = Math.max(this.timeService.getMinDatapointsLimit(), Math.floor(this.timeService.getMaxDatapointsLimit() / 10)); + readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; + readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], @@ -72,7 +75,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]], }), defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], - limit: [this.defaultLimit], + limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }], timeWindow: [MINUTE * 15], }); @@ -96,7 +99,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit { private fb: FormBuilder, private cd: ChangeDetectorRef, private popover: TbPopoverComponent, - private timeService: TimeService + private store: Store ) { this.observeEntityFilterChanges(); this.observeEntityTypeChanges() diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 9f1839afec..64d5039914 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -229,6 +229,92 @@ +
+ + {{ 'tenant-profile.calculated-fields' | translate }} tenant-profile.unlimited + +
+ + tenant-profile.max-calculated-fields + + + {{ 'tenant-profile.max-calculated-fields-required' | translate}} + + + {{ 'tenant-profile.max-calculated-fields-range' | translate}} + + + + + tenant-profile.max-data-points-per-rolling-arg + + + {{ 'tenant-profile.max-data-points-per-rolling-arg-required' | translate}} + + + {{ 'tenant-profile.max-data-points-per-rolling-arg-range' | translate}} + + + +
+
+ + tenant-profile.max-arguments-per-cf + + + {{ 'tenant-profile.max-arguments-per-cf-required' | translate}} + + + {{ 'tenant-profile.max-arguments-per-cf-range' | translate}} + + + +
+
+ + + + tenant-profile.advanced-settings + + + +
+ + tenant-profile.max-state-size + + + {{ 'tenant-profile.max-state-size-required' | translate}} + + + {{ 'tenant-profile.max-state-size-range' | translate}} + + + + + tenant-profile.max-value-argument-size + + + {{ 'tenant-profile.max-value-argument-size-required' | translate}} + + + {{ 'tenant-profile.max-value-argument-size-range' | translate}} + + + +
+
+
+
@@ -638,6 +724,12 @@ [type]="rateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT"> +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index 4cce34c502..b1d6652e4a 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -118,7 +118,13 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA edgeEventRateLimits: [null, []], edgeEventRateLimitsPerEdge: [null, []], edgeUplinkMessagesRateLimits: [null, []], - edgeUplinkMessagesRateLimitsPerEdge: [null, []] + edgeUplinkMessagesRateLimitsPerEdge: [null, []], + maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], + maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], + maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], + maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], + calculatedFieldDebugEventsRateLimit: [null, []], + maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]], }); this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts index ab50c967bf..f09f950ee7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts @@ -45,7 +45,8 @@ export enum RateLimitsType { EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT', EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT', EDGE_UPLINK_MESSAGES_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_RATE_LIMIT', - EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT' + EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT', + CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT = 'CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT', } export const rateLimitsLabelTranslationMap = new Map( @@ -74,6 +75,7 @@ export const rateLimitsLabelTranslationMap = new Map( [RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-rate-limit'], [RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-per-edge-rate-limit'], + [RateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT, 'tenant-profile.rate-limits.calculated-field-debug-event-rate-limit'], ] ); @@ -103,6 +105,7 @@ export const rateLimitsDialogTitleTranslationMap = new Map Date: Fri, 28 Feb 2025 11:49:00 +0200 Subject: [PATCH 30/42] BaseRuleChainProcessor - use saveRuleChain method without validation --- .../service/edge/rpc/processor/rule/BaseRuleChainProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java index 5156956c3d..00b9c732aa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/BaseRuleChainProcessor.java @@ -61,7 +61,7 @@ public class BaseRuleChainProcessor extends BaseEdgeProcessor { if (created) { ruleChain.setId(ruleChainId); } - edgeCtx.getRuleChainService().saveRuleChain(ruleChain); + edgeCtx.getRuleChainService().saveRuleChain(ruleChain, true, false); return Pair.of(created, isRoot); } From 8e8ecf9a49f5fe38b407b4b4090fed430eedaf8f Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 11:54:38 +0200 Subject: [PATCH 31/42] refactoring --- .../calculated-field-arguments-table.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 5510aeb2c2..6bab01f976 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -126,11 +126,11 @@ color="primary" #button (click)="manageArgument($event, button)" - [disabled]="!!maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF" + [disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF" > {{ 'calculated-fields.add-argument' | translate }} - @if (!!maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF) { + @if (maxArgumentsPerCF && argumentsFormArray.length >= maxArgumentsPerCF) {
warning {{ 'calculated-fields.hint.max-args' | translate }} From 5fb5d008bfe483bcd9b2bbc59fa730dcba6102ad Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 28 Feb 2025 12:33:29 +0200 Subject: [PATCH 32/42] fixed tests --- .../dao/model/sql/CalculatedFieldLinkEntity.java | 11 +++++++++-- .../server/dao/service/DeviceServiceTest.java | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java index 2668a1a0d8..3a529b7d93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; @@ -60,6 +58,15 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity Date: Fri, 28 Feb 2025 12:34:47 +0200 Subject: [PATCH 33/42] Added tenant config model and default value --- ui-ngx/src/app/shared/models/tenant.model.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 23c2d95762..38ff63e3c6 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -95,6 +95,13 @@ export interface DefaultTenantProfileConfiguration { rpcTtlDays: number; queueStatsTtlDays: number; ruleEngineExceptionsTtlDays: number; + + maxCalculatedFieldsPerEntity: number; + maxArgumentsPerCF: number; + maxDataPointsPerRollingArg: number; + maxStateSizeInKBytes: number; + maxSingleValueArgumentSizeInKBytes: number; + calculatedFieldDebugEventsRateLimit: string; } export type TenantProfileConfigurations = DefaultTenantProfileConfiguration; @@ -148,7 +155,13 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan alarmsTtlDays: 0, rpcTtlDays: 0, queueStatsTtlDays: 0, - ruleEngineExceptionsTtlDays: 0 + ruleEngineExceptionsTtlDays: 0, + maxCalculatedFieldsPerEntity: 0, + maxArgumentsPerCF: 0, + maxDataPointsPerRollingArg: 0, + maxStateSizeInKBytes: 0, + maxSingleValueArgumentSizeInKBytes: 0, + calculatedFieldDebugEventsRateLimit: '' }; configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; break; From 51e48e20911f9efcd3be4a545fe80ac563b4e007 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 28 Feb 2025 15:00:07 +0200 Subject: [PATCH 34/42] added endpoint to get latest cf debug event --- .../controller/CalculatedFieldController.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 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 fbeade5819..fe85cf3f87 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -35,19 +35,22 @@ 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.TbelInvokeService; -import org.thingsboard.server.common.data.Device; 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; 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; @@ -61,6 +64,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; @@ -84,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI 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"; @@ -176,14 +181,31 @@ public class CalculatedFieldController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedField) throws Exception { - checkParameter(CALCULATED_FIELD_ID, strCalculatedField); - CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedField)); + public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { + checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.DELETE); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); } + @ApiOperation(value = "Get latest calculated field debug event (getLatestCalculatedFieldDebugEvent)", + 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 + public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { + checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); + CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.READ); + checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); + TenantId tenantId = getCurrentUser().getTenantId(); + return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) + .flatMap(events -> events.stream().map(EventInfo::getBody).findFirst()) + .orElse(null); + } + @ApiOperation(value = "Test Script expression", notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") From 0e819850c83a2c5bc27d894cd312a61efc02545a Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 28 Feb 2025 15:11:21 +0200 Subject: [PATCH 35/42] lwm2m: fix bug monitoring --- .../transport/TransportHealthChecker.java | 2 +- .../main/resources/lwm2m/device_profile.json | 6 +- .../resources/lwm2m/models/test-model.xml | 324 +++++++++++++++++- .../src/main/resources/lwm2m/resource.json | 2 +- pom.xml | 2 + 5 files changed, 312 insertions(+), 24 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index 1b8a704fad..f5e6cb5469 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -145,7 +145,7 @@ public abstract class TransportHealthChecker { TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json index c75fe9ce7a..d23e02825d 100644 --- a/monitoring/src/main/resources/lwm2m/device_profile.json +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -12,14 +12,14 @@ "transportConfiguration": { "observeAttr": { "observe": [ - "/3_1.0/0/0" + "/3_1.1/0/0" ], "attribute": [], "telemetry": [ - "/3_1.0/0/0" + "/3_1.1/0/0" ], "keyName": { - "/3_1.0/0/0": "testData" + "/3_1.1/0/0": "testData" }, "attributeLwm2m": {} }, diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml index 2d25f4698e..02b084127f 100644 --- a/monitoring/src/main/resources/lwm2m/models/test-model.xml +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -1,45 +1,331 @@ + - + + LwM2M Monitoring - - + 3 - urn:oma:lwm2m:oma:3 + urn:oma:lwm2m:oma:3:1.1 1.1 - 1.0 + 1.1 Single Mandatory - Test data + Manufacturer + R + Single + Optional + String + + + + + + Model Number R Single Optional String - + - + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + /100 + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..32 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json index dcc0182bdd..bbef8da7ec 100644 --- a/monitoring/src/main/resources/lwm2m/resource.json +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -2,5 +2,5 @@ "title": "", "resourceType": "LWM2M_MODEL", "fileName": "test-model.xml", - "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjQgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozPC9PYmplY3RVUk4+CiAgICAgICAgPExXTTJNVmVyc2lvbj4xLjE8L0xXTTJNVmVyc2lvbj4KICAgICAgICA8T2JqZWN0VmVyc2lvbj4xLjA8L09iamVjdFZlcnNpb24+CiAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgPE1hbmRhdG9yeT5NYW5kYXRvcnk8L01hbmRhdG9yeT4KICAgICAgICA8UmVzb3VyY2VzPgogICAgICAgICAgICA8SXRlbSBJRD0iMCI+CiAgICAgICAgICAgICAgICA8TmFtZT5UZXN0IGRhdGE8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUZXN0IGRhdGFdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgPC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KCjwhLS0KRklMRSBJTkZPUk1BVElPTgoKT01BIFBlcm1hbmVudCBEb2N1bWVudAogICBGaWxlOiBPTUEtU1VQLVhNTF8zLVYxXzItMjAyMDExMTAtQS54bWwKICAgUGF0aDogaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3JlbGVhc2UvT2JqTHdNMk1fRGV2aWNlLwoKT01OQSBMd00yTSBSZWdpc3RyeQogICBQYXRoOiBodHRwczovL2dpdGh1Yi5jb20vT3Blbk1vYmlsZUFsbGlhbmNlL2x3bTJtLXJlZ2lzdHJ5CiAgIE5hbWU6IDMueG1sCgpOT1JNQVRJVkUgSU5GT1JNQVRJT04KCiAgSW5mb3JtYXRpb24gYWJvdXQgdGhpcyBmaWxlIGNhbiBiZSBmb3VuZCBpbiB0aGUgbGF0ZXN0IHJldmlzaW9uIG9mCgogICAgT01BLVRTLUxpZ2h0d2VpZ2h0TTJNX0NvcmUtVjFfMgoKICBUaGlzIGlzIGF2YWlsYWJsZSBhdCBodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvcmVsZWFzZS9MaWdodHdlaWdodE0yTS8KCiAgU2VuZCBjb21tZW50cyB0byBodHRwczovL2dpdGh1Yi5jb20vT3Blbk1vYmlsZUFsbGlhbmNlL09NQV9Md00yTV9mb3JfRGV2ZWxvcGVycy9pc3N1ZXMKCkxFR0FMIERJU0NMQUlNRVIKCiAgQ29weXJpZ2h0IDIwMjAgT3BlbiBNb2JpbGUgQWxsaWFuY2UuCgogIFJlZGlzdHJpYnV0aW9uIGFuZCB1c2UgaW4gc291cmNlIGFuZCBiaW5hcnkgZm9ybXMsIHdpdGggb3Igd2l0aG91dAogIG1vZGlmaWNhdGlvbiwgYXJlIHBlcm1pdHRlZCBwcm92aWRlZCB0aGF0IHRoZSBmb2xsb3dpbmcgY29uZGl0aW9ucwogIGFyZSBtZXQ6CgogIDEuIFJlZGlzdHJpYnV0aW9ucyBvZiBzb3VyY2UgY29kZSBtdXN0IHJldGFpbiB0aGUgYWJvdmUgY29weXJpZ2h0CiAgbm90aWNlLCB0aGlzIGxpc3Qgb2YgY29uZGl0aW9ucyBhbmQgdGhlIGZvbGxvd2luZyBkaXNjbGFpbWVyLgogIDIuIFJlZGlzdHJpYnV0aW9ucyBpbiBiaW5hcnkgZm9ybSBtdXN0IHJlcHJvZHVjZSB0aGUgYWJvdmUgY29weXJpZ2h0CiAgbm90aWNlLCB0aGlzIGxpc3Qgb2YgY29uZGl0aW9ucyBhbmQgdGhlIGZvbGxvd2luZyBkaXNjbGFpbWVyIGluIHRoZQogIGRvY3VtZW50YXRpb24gYW5kL29yIG90aGVyIG1hdGVyaWFscyBwcm92aWRlZCB3aXRoIHRoZSBkaXN0cmlidXRpb24uCiAgMy4gTmVpdGhlciB0aGUgbmFtZSBvZiB0aGUgY29weXJpZ2h0IGhvbGRlciBub3IgdGhlIG5hbWVzIG9mIGl0cwogIGNvbnRyaWJ1dG9ycyBtYXkgYmUgdXNlZCB0byBlbmRvcnNlIG9yIHByb21vdGUgcHJvZHVjdHMgZGVyaXZlZAogIGZyb20gdGhpcyBzb2Z0d2FyZSB3aXRob3V0IHNwZWNpZmljIHByaW9yIHdyaXR0ZW4gcGVybWlzc2lvbi4KCiAgVEhJUyBTT0ZUV0FSRSBJUyBQUk9WSURFRCBCWSBUSEUgQ09QWVJJR0hUIEhPTERFUlMgQU5EIENPTlRSSUJVVE9SUwogICJBUyBJUyIgQU5EIEFOWSBFWFBSRVNTIE9SIElNUExJRUQgV0FSUkFOVElFUywgSU5DTFVESU5HLCBCVVQgTk9UCiAgTElNSVRFRCBUTywgVEhFIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MKICBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQVJFIERJU0NMQUlNRUQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRQogIENPUFlSSUdIVCBIT0xERVIgT1IgQ09OVFJJQlVUT1JTIEJFIExJQUJMRSBGT1IgQU5ZIERJUkVDVCwgSU5ESVJFQ1QsCiAgSU5DSURFTlRBTCwgU1BFQ0lBTCwgRVhFTVBMQVJZLCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgKElOQ0xVRElORywKICBCVVQgTk9UIExJTUlURUQgVE8sIFBST0NVUkVNRU5UIE9GIFNVQlNUSVRVVEUgR09PRFMgT1IgU0VSVklDRVM7CiAgTE9TUyBPRiBVU0UsIERBVEEsIE9SIFBST0ZJVFM7IE9SIEJVU0lORVNTIElOVEVSUlVQVElPTikgSE9XRVZFUgogIENBVVNFRCBBTkQgT04gQU5ZIFRIRU9SWSBPRiBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQ09OVFJBQ1QsIFNUUklDVAogIExJQUJJTElUWSwgT1IgVE9SVCAoSU5DTFVESU5HIE5FR0xJR0VOQ0UgT1IgT1RIRVJXSVNFKSBBUklTSU5HIElOCiAgQU5ZIFdBWSBPVVQgT0YgVEhFIFVTRSBPRiBUSElTIFNPRlRXQVJFLCBFVkVOIElGIEFEVklTRUQgT0YgVEhFCiAgUE9TU0lCSUxJVFkgT0YgU1VDSCBEQU1BR0UuCgogIFRoZSBhYm92ZSBsaWNlbnNlIGlzIHVzZWQgYXMgYSBsaWNlbnNlIHVuZGVyIGNvcHlyaWdodCBvbmx5LiAgUGxlYXNlCiAgcmVmZXJlbmNlIHRoZSBPTUEgSVBSIFBvbGljeSBmb3IgcGF0ZW50IGxpY2Vuc2luZyB0ZXJtczoKICBodHRwczovL3d3dy5vbWFzcGVjd29ya3Mub3JnL2Fib3V0L2ludGVsbGVjdHVhbC1wcm9wZXJ0eS1yaWdodHMvCgotLT4KCjxMV00yTSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6bm9OYW1lc3BhY2VTY2hlbWFMb2NhdGlvbj0iaHR0cDovL3d3dy5vcGVubW9iaWxlYWxsaWFuY2Uub3JnL3RlY2gvcHJvZmlsZXMvTFdNMk0tdjFfMS54c2QiPgogICAgPE9iamVjdCBPYmplY3RUeXBlPSJNT0RlZmluaXRpb24iPgogICAgICAgIDxOYW1lPkx3TTJNIE1vbml0b3Jpbmc8L05hbWU+CiAgICAgICAgPERlc2NyaXB0aW9uMT48IVtDREFUQVtUaGlzIEx3TTJNIE9iamVjdCBwcm92aWRlcyBhIHJhbmdlIG9mIGRldmljZSByZWxhdGVkIGluZm9ybWF0aW9uIHdoaWNoIGNhbiBiZSBxdWVyaWVkIGJ5IHRoZSBMd00yTSBTZXJ2ZXIsIGFuZCBhIGRldmljZSByZWJvb3QgYW5kIGZhY3RvcnkgcmVzZXQgZnVuY3Rpb24uXV0+PC9EZXNjcmlwdGlvbjE+CiAgICAgICAgPE9iamVjdElEPjM8L09iamVjdElEPgogICAgICAgIDxPYmplY3RVUk4+dXJuOm9tYTpsd20ybTpvbWE6MzoxLjE8L09iamVjdFVSTj4KICAgICAgICA8TFdNMk1WZXJzaW9uPjEuMTwvTFdNMk1WZXJzaW9uPgogICAgICAgIDxPYmplY3RWZXJzaW9uPjEuMTwvT2JqZWN0VmVyc2lvbj4KICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgIDxSZXNvdXJjZXM+CiAgICAgICAgICAgIDxJdGVtIElEPSIwIj4KICAgICAgICAgICAgICAgIDxOYW1lPk1hbnVmYWN0dXJlcjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0h1bWFuIHJlYWRhYmxlIG1hbnVmYWN0dXJlciBuYW1lXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMSI+CiAgICAgICAgICAgICAgICA8TmFtZT5Nb2RlbCBOdW1iZXI8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtBIG1vZGVsIGlkZW50aWZpZXIgKG1hbnVmYWN0dXJlciBzcGVjaWZpZWQgc3RyaW5nKV1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjIiPgogICAgICAgICAgICAgICAgPE5hbWU+U2VyaWFsIE51bWJlcjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1NlcmlhbCBOdW1iZXJdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIzIj4KICAgICAgICAgICAgICAgIDxOYW1lPkZpcm13YXJlIFZlcnNpb248L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtDdXJyZW50IGZpcm13YXJlIHZlcnNpb24gb2YgdGhlIERldmljZS5UaGUgRmlybXdhcmUgTWFuYWdlbWVudCBmdW5jdGlvbiBjb3VsZCByZWx5IG9uIHRoaXMgcmVzb3VyY2UuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iNCI+CiAgICAgICAgICAgICAgICA8TmFtZT5SZWJvb3Q8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5FPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+PC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbUmVib290IHRoZSBMd00yTSBEZXZpY2UgdG8gcmVzdG9yZSB0aGUgRGV2aWNlIGZyb20gdW5leHBlY3RlZCBmaXJtd2FyZSBmYWlsdXJlLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjUiPgogICAgICAgICAgICAgICAgPE5hbWU+RmFjdG9yeSBSZXNldDwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPkU8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1BlcmZvcm0gZmFjdG9yeSByZXNldCBvZiB0aGUgTHdNMk0gRGV2aWNlIHRvIG1ha2UgdGhlIEx3TTJNIERldmljZSB0byBnbyB0aHJvdWdoIGluaXRpYWwgZGVwbG95bWVudCBzZXF1ZW5jZSB3aGVyZSBwcm92aXNpb25pbmcgYW5kIGJvb3RzdHJhcCBzZXF1ZW5jZSBpcyBwZXJmb3JtZWQuIFRoaXMgcmVxdWlyZXMgY2xpZW50IGVuc3VyaW5nIHBvc3QgZmFjdG9yeSByZXNldCB0byBoYXZlIG1pbmltYWwgaW5mb3JtYXRpb24gdG8gYWxsb3cgaXQgdG8gY2Fycnkgb3V0IG9uZSBvZiB0aGUgYm9vdHN0cmFwIG1ldGhvZHMgc3BlY2lmaWVkIGluIHNlY3Rpb24gNS4yLjMuCldoZW4gdGhpcyBSZXNvdXJjZSBpcyBleGVjdXRlZCwgIkRlLXJlZ2lzdGVyIiBvcGVyYXRpb24gTUFZIGJlIHNlbnQgdG8gdGhlIEx3TTJNIFNlcnZlcihzKSBiZWZvcmUgZmFjdG9yeSByZXNldCBvZiB0aGUgTHdNMk0gRGV2aWNlLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjYiPgogICAgICAgICAgICAgICAgPE5hbWU+QXZhaWxhYmxlIFBvd2VyIFNvdXJjZXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPkludGVnZXI8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj4wLi43PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbMDogREMgcG93ZXIKMTogSW50ZXJuYWwgQmF0dGVyeQoyOiBFeHRlcm5hbCBCYXR0ZXJ5CjM6IEZ1ZWwgQ2VsbAo0OiBQb3dlciBvdmVyIEV0aGVybmV0CjU6IFVTQgo2OiBBQyAoTWFpbnMpIHBvd2VyCjc6IFNvbGFyClRoZSBzYW1lIFJlc291cmNlIEluc3RhbmNlIElEIE1VU1QgYmUgdXNlZCB0byBhc3NvY2lhdGUgYSBnaXZlbiBQb3dlciBTb3VyY2UgKFJlc291cmNlIElEOjYpIHdpdGggaXRzIFByZXNlbnQgVm9sdGFnZSAoUmVzb3VyY2UgSUQ6NykgYW5kIGl0cyBQcmVzZW50IEN1cnJlbnQgKFJlc291cmNlIElEOjgpXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iNyI+CiAgICAgICAgICAgICAgICA8TmFtZT5Qb3dlciBTb3VyY2UgVm9sdGFnZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+TXVsdGlwbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+SW50ZWdlcjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1ByZXNlbnQgdm9sdGFnZSBmb3IgZWFjaCBBdmFpbGFibGUgUG93ZXIgU291cmNlcyBSZXNvdXJjZSBJbnN0YW5jZS4gVGhlIHVuaXQgdXNlZCBmb3IgdGhpcyByZXNvdXJjZSBpcyBpbiBtVi5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSI4Ij4KICAgICAgICAgICAgICAgIDxOYW1lPlBvd2VyIFNvdXJjZSBDdXJyZW50PC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5NdWx0aXBsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbUHJlc2VudCBjdXJyZW50IGZvciBlYWNoIEF2YWlsYWJsZSBQb3dlciBTb3VyY2UuIFRoZSB1bml0IHVzZWQgZm9yIHRoaXMgcmVzb3VyY2UgaXMgaW4gbUEuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iOSI+CiAgICAgICAgICAgICAgICA8TmFtZT5CYXR0ZXJ5IExldmVsPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+SW50ZWdlcjwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjAuLjEwMDwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz4vMTAwPC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtDb250YWlucyB0aGUgY3VycmVudCBiYXR0ZXJ5IGxldmVsIGFzIGEgcGVyY2VudGFnZSAod2l0aCBhIHJhbmdlIGZyb20gMCB0byAxMDApLiBUaGlzIHZhbHVlIGlzIG9ubHkgdmFsaWQgZm9yIHRoZSBEZXZpY2UgaW50ZXJuYWwgQmF0dGVyeSBpZiBwcmVzZW50IChvbmUgQXZhaWxhYmxlIFBvd2VyIFNvdXJjZXMgUmVzb3VyY2UgSW5zdGFuY2UgaXMgMSkuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMTAiPgogICAgICAgICAgICAgICAgPE5hbWU+TWVtb3J5IEZyZWU8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbRXN0aW1hdGVkIGN1cnJlbnQgYXZhaWxhYmxlIGFtb3VudCBvZiBzdG9yYWdlIHNwYWNlIHdoaWNoIGNhbiBzdG9yZSBkYXRhIGFuZCBzb2Z0d2FyZSBpbiB0aGUgTHdNMk0gRGV2aWNlIChleHByZXNzZWQgaW4ga2lsb2J5dGVzKS4gTm90ZTogMSBraWxvYnl0ZSBjb3JyZXNwb25kcyB0byAxMDAwIGJ5dGVzLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjExIj4KICAgICAgICAgICAgICAgIDxOYW1lPkVycm9yIENvZGU8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+MC4uMzI8L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVswPU5vIGVycm9yCjE9TG93IGJhdHRlcnkgcG93ZXIKMj1FeHRlcm5hbCBwb3dlciBzdXBwbHkgb2ZmCjM9R1BTIG1vZHVsZSBmYWlsdXJlCjQ9TG93IHJlY2VpdmVkIHNpZ25hbCBzdHJlbmd0aAo1PU91dCBvZiBtZW1vcnkKNj1TTVMgZmFpbHVyZQo3PUlQIGNvbm5lY3Rpdml0eSBmYWlsdXJlCjg9UGVyaXBoZXJhbCBtYWxmdW5jdGlvbgo5Li4xNT1SZXNlcnZlZCBmb3IgZnV0dXJlIHVzZQoxNi4uMzI9RGV2aWNlIHNwZWNpZmljIGVycm9yIGNvZGVzCgpXaGVuIHRoZSBzaW5nbGUgRGV2aWNlIE9iamVjdCBJbnN0YW5jZSBpcyBpbml0aWF0ZWQsIHRoZXJlIGlzIG9ubHkgb25lIGVycm9yIGNvZGUgUmVzb3VyY2UgSW5zdGFuY2Ugd2hvc2UgdmFsdWUgaXMgZXF1YWwgdG8gMCB0aGF0IG1lYW5zIG5vIGVycm9yLiBXaGVuIHRoZSBmaXJzdCBlcnJvciBoYXBwZW5zLCB0aGUgTHdNMk0gQ2xpZW50IGNoYW5nZXMgZXJyb3IgY29kZSBSZXNvdXJjZSBJbnN0YW5jZSB0byBhbnkgbm9uLXplcm8gdmFsdWUgdG8gaW5kaWNhdGUgdGhlIGVycm9yIHR5cGUuIFdoZW4gYW55IG90aGVyIGVycm9yIGhhcHBlbnMsIGEgbmV3IGVycm9yIGNvZGUgUmVzb3VyY2UgSW5zdGFuY2UgaXMgY3JlYXRlZC4gV2hlbiBhbiBlcnJvciBhc3NvY2lhdGVkIHdpdGggYSBSZXNvdXJjZSBJbnN0YW5jZSBpcyBubyBsb25nZXIgcHJlc2VudCwgdGhhdCBSZXNvdXJjZSBJbnN0YW5jZSBpcyBkZWxldGVkLiBXaGVuIHRoZSBzaW5nbGUgZXhpc3RpbmcgZXJyb3IgaXMgbm8gbG9uZ2VyIHByZXNlbnQsIHRoZSBMd00yTSBDbGllbnQgcmV0dXJucyB0byB0aGUgb3JpZ2luYWwgbm8gZXJyb3Igc3RhdGUgd2hlcmUgSW5zdGFuY2UgMCBoYXMgdmFsdWUgMC4KVGhpcyBlcnJvciBjb2RlIFJlc291cmNlIE1BWSBiZSBvYnNlcnZlZCBieSB0aGUgTHdNMk0gU2VydmVyLiBIb3cgdG8gZGVhbCB3aXRoIEx3TTJNIENsaWVudOKAmXMgZXJyb3IgcmVwb3J0IGRlcGVuZHMgb24gdGhlIHBvbGljeSBvZiB0aGUgTHdNMk0gU2VydmVyLiBFcnJvciBjb2RlcyBpbiBiZXR3ZWVuIDE2IGFuZCAzMiBhcmUgc3BlY2lmaWMgdG8gdGhlIERldmljZSBhbmQgbWF5IGhhdmUgZGlmZmVyZW50IG1lYW5pbmdzIGFtb25nIGltcGxlbWVudGF0aW9ucy5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxMiI+CiAgICAgICAgICAgICAgICA8TmFtZT5SZXNldCBFcnJvciBDb2RlPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+RTwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+PC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbRGVsZXRlIGFsbCBlcnJvciBjb2RlIFJlc291cmNlIEluc3RhbmNlcyBhbmQgY3JlYXRlIG9ubHkgb25lIHplcm8tdmFsdWUgZXJyb3IgY29kZSB0aGF0IGltcGxpZXMgbm8gZXJyb3IsIHRoZW4gcmUtZXZhbHVhdGUgYWxsIGVycm9yIGNvbmRpdGlvbnMgYW5kIHVwZGF0ZSBhbmQgY3JlYXRlIFJlc291cmNlcyBJbnN0YW5jZXMgdG8gY2FwdHVyZSBhbGwgY3VycmVudCBlcnJvciBjb25kaXRpb25zLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjEzIj4KICAgICAgICAgICAgICAgIDxOYW1lPkN1cnJlbnQgVGltZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5UaW1lPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbQ3VycmVudCBVTklYIHRpbWUgb2YgdGhlIEx3TTJNIENsaWVudC4KVGhlIEx3TTJNIENsaWVudCBzaG91bGQgYmUgcmVzcG9uc2libGUgdG8gaW5jcmVhc2UgdGhpcyB0aW1lIHZhbHVlIGFzIGV2ZXJ5IHNlY29uZCBlbGFwc2VzLgpUaGUgTHdNMk0gU2VydmVyIGlzIGFibGUgdG8gd3JpdGUgdGhpcyBSZXNvdXJjZSB0byBtYWtlIHRoZSBMd00yTSBDbGllbnQgc3luY2hyb25pemVkIHdpdGggdGhlIEx3TTJNIFNlcnZlci5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNCI+CiAgICAgICAgICAgICAgICA8TmFtZT5VVEMgT2Zmc2V0PC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+Ulc8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0luZGljYXRlcyB0aGUgVVRDIG9mZnNldCBjdXJyZW50bHkgaW4gZWZmZWN0IGZvciB0aGlzIEx3TTJNIERldmljZS4gVVRDK1ggW0lTTyA4NjAxXS5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNSI+CiAgICAgICAgICAgICAgICA8TmFtZT5UaW1lem9uZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlJXPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5TdHJpbmc8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtJbmRpY2F0ZXMgaW4gd2hpY2ggdGltZSB6b25lIHRoZSBMd00yTSBEZXZpY2UgaXMgbG9jYXRlZCwgaW4gSUFOQSBUaW1lem9uZSAoVFopIGRhdGFiYXNlIGZvcm1hdC5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxNiI+CiAgICAgICAgICAgICAgICA8TmFtZT5TdXBwb3J0ZWQgQmluZGluZyBhbmQgTW9kZXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk1hbmRhdG9yeTwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbSW5kaWNhdGVzIHdoaWNoIGJpbmRpbmdzIGFuZCBtb2RlcyBhcmUgc3VwcG9ydGVkIGluIHRoZSBMd00yTSBDbGllbnQuIFRoZSBwb3NzaWJsZSB2YWx1ZXMgYXJlIHRob3NlIGxpc3RlZCBpbiB0aGUgTHdNMk0gQ29yZSBTcGVjaWZpY2F0aW9uLl1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjE3Ij48TmFtZT5EZXZpY2UgVHlwZTwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1R5cGUgb2YgdGhlIGRldmljZSAobWFudWZhY3R1cmVyIHNwZWNpZmllZCBzdHJpbmc6IGUuZy4gc21hcnQgbWV0ZXJzIC8gZGV2IENsYXNzIC8gLi4uKV1dPjwvRGVzY3JpcHRpb24+CiAgICAgICAgICAgIDwvSXRlbT4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjE4Ij48TmFtZT5IYXJkd2FyZSBWZXJzaW9uPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbQ3VycmVudCBoYXJkd2FyZSB2ZXJzaW9uIG9mIHRoZSBkZXZpY2VdXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIxOSI+PE5hbWU+U29mdHdhcmUgVmVyc2lvbjwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPlN0cmluZzwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW0N1cnJlbnQgc29mdHdhcmUgdmVyc2lvbiBvZiB0aGUgZGV2aWNlIChtYW51ZmFjdHVyZXIgc3BlY2lmaWVkIHN0cmluZykuIE9uIGVsYWJvcmF0ZWQgTHdNMk0gZGV2aWNlLCBTVyBjb3VsZCBiZSBzcGxpdCBpbiAyIHBhcnRzOiBhIGZpcm13YXJlIG9uZSBhbmQgYSBoaWdoZXIgbGV2ZWwgc29mdHdhcmUgb24gdG9wLgpCb3RoIHBpZWNlcyBvZiBTb2Z0d2FyZSBhcmUgdG9nZXRoZXIgbWFuYWdlZCBieSBMd00yTSBGaXJtd2FyZSBVcGRhdGUgT2JqZWN0IChPYmplY3QgSUQgNSldXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+CiAgICAgICAgICAgIDxJdGVtIElEPSIyMCI+PE5hbWU+QmF0dGVyeSBTdGF0dXM8L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPlNpbmdsZTwvTXVsdGlwbGVJbnN0YW5jZXM+CiAgICAgICAgICAgICAgICA8TWFuZGF0b3J5Pk9wdGlvbmFsPC9NYW5kYXRvcnk+CiAgICAgICAgICAgICAgICA8VHlwZT5JbnRlZ2VyPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+MC4uNjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1RoaXMgdmFsdWUgaXMgb25seSB2YWxpZCBmb3IgdGhlIERldmljZSBJbnRlcm5hbCBCYXR0ZXJ5IGlmIHByZXNlbnQgKG9uZSBBdmFpbGFibGUgUG93ZXIgU291cmNlcyBSZXNvdXJjZSBJbnN0YW5jZSB2YWx1ZSBpcyAxKS4KQmF0dGVyeQpTdGF0dXMJTWVhbmluZwlEZXNjcmlwdGlvbgowCU5vcm1hbAlUaGUgYmF0dGVyeSBpcyBvcGVyYXRpbmcgbm9ybWFsbHkgYW5kIG5vdCBvbiBwb3dlci4KMQlDaGFyZ2luZwlUaGUgYmF0dGVyeSBpcyBjdXJyZW50bHkgY2hhcmdpbmcuCjIJQ2hhcmdlIENvbXBsZXRlCVRoZSBiYXR0ZXJ5IGlzIGZ1bGx5IGNoYXJnZWQgYW5kIHN0aWxsIG9uIHBvd2VyLgozCURhbWFnZWQJVGhlIGJhdHRlcnkgaGFzIHNvbWUgcHJvYmxlbS4KNAlMb3cgQmF0dGVyeQlUaGUgYmF0dGVyeSBpcyBsb3cgb24gY2hhcmdlLgo1CU5vdCBJbnN0YWxsZWQJVGhlIGJhdHRlcnkgaXMgbm90IGluc3RhbGxlZC4KNglVbmtub3duCVRoZSBiYXR0ZXJ5IGluZm9ybWF0aW9uIGlzIG5vdCBhdmFpbGFibGUuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMjEiPjxOYW1lPk1lbW9yeSBUb3RhbDwvTmFtZT4KICAgICAgICAgICAgICAgIDxPcGVyYXRpb25zPlI8L09wZXJhdGlvbnM+CiAgICAgICAgICAgICAgICA8TXVsdGlwbGVJbnN0YW5jZXM+U2luZ2xlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPkludGVnZXI8L1R5cGU+CiAgICAgICAgICAgICAgICA8UmFuZ2VFbnVtZXJhdGlvbj48L1JhbmdlRW51bWVyYXRpb24+CiAgICAgICAgICAgICAgICA8VW5pdHM+PC9Vbml0cz4KICAgICAgICAgICAgICAgIDxEZXNjcmlwdGlvbj48IVtDREFUQVtUb3RhbCBhbW91bnQgb2Ygc3RvcmFnZSBzcGFjZSB3aGljaCBjYW4gc3RvcmUgZGF0YSBhbmQgc29mdHdhcmUgaW4gdGhlIEx3TTJNIERldmljZSAoZXhwcmVzc2VkIGluIGtpbG9ieXRlcykuICBOb3RlOiAxIGtpbG9ieXRlIGNvcnJlc3BvbmRzIHRvIDEwMDAgYnl0ZXMuXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgICAgICA8SXRlbSBJRD0iMjIiPjxOYW1lPkV4dERldkluZm88L05hbWU+CiAgICAgICAgICAgICAgICA8T3BlcmF0aW9ucz5SPC9PcGVyYXRpb25zPgogICAgICAgICAgICAgICAgPE11bHRpcGxlSW5zdGFuY2VzPk11bHRpcGxlPC9NdWx0aXBsZUluc3RhbmNlcz4KICAgICAgICAgICAgICAgIDxNYW5kYXRvcnk+T3B0aW9uYWw8L01hbmRhdG9yeT4KICAgICAgICAgICAgICAgIDxUeXBlPk9iamxuazwvVHlwZT4KICAgICAgICAgICAgICAgIDxSYW5nZUVudW1lcmF0aW9uPjwvUmFuZ2VFbnVtZXJhdGlvbj4KICAgICAgICAgICAgICAgIDxVbml0cz48L1VuaXRzPgogICAgICAgICAgICAgICAgPERlc2NyaXB0aW9uPjwhW0NEQVRBW1JlZmVyZW5jZSB0byBleHRlcm5hbCAiRGV2aWNlIiBvYmplY3QgaW5zdGFuY2UgY29udGFpbmluZyBpbmZvcm1hdGlvbi4gRm9yIGV4YW1wbGUsIHN1Y2ggYW4gZXh0ZXJuYWwgZGV2aWNlIGNhbiBiZSBhIEhvc3QgRGV2aWNlLCB3aGljaCBpcyBhIGRldmljZSBpbnRvIHdoaWNoIHRoZSBEZXZpY2UgY29udGFpbmluZyB0aGUgTHdNMk0gY2xpZW50IGlzIGVtYmVkZGVkLiBUaGlzIFJlc291cmNlIG1heSBiZSB1c2VkIHRvIHJldHJpZXZlIGluZm9ybWF0aW9uIGFib3V0IHRoZSBIb3N0IERldmljZS5dXT48L0Rlc2NyaXB0aW9uPgogICAgICAgICAgICA8L0l0ZW0+PC9SZXNvdXJjZXM+CiAgICAgICAgPERlc2NyaXB0aW9uMj48L0Rlc2NyaXB0aW9uMj4KICAgIDwvT2JqZWN0Pgo8L0xXTTJNPgo=" } \ No newline at end of file diff --git a/pom.xml b/pom.xml index b851b8fc10..736c49e80f 100755 --- a/pom.xml +++ b/pom.xml @@ -848,6 +848,8 @@ .run/** **/NetworkReceive.java **/lwm2m-registry/** + **/test/resources/lwm2m/** + **/resources/lwm2m/models/** src/main/data/resources/** From 96f7a90a7b9ec2dacf8bf616c8754edfcfcbb948 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 15:15:23 +0200 Subject: [PATCH 36/42] Added rolling argument functions autocompletes --- .../calculated-field-dialog.component.html | 5 +- .../calculated-field-dialog.component.scss | 6 +- ...ed-field-script-test-dialog.component.scss | 2 +- .../shared/models/calculated-field.models.ts | 156 ++++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 3 +- 5 files changed, 168 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 0695b9d2b9..7de7bce749 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -78,7 +78,7 @@
{{ 'calculated-fields.expression' | translate }}
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - + @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { @@ -91,6 +91,9 @@ } + @if (!configFormGroup.get('expressionSIMPLE').errors || !configFormGroup.get('expressionSIMPLE').touched) { + {{ 'calculated-fields.hint.expression' | translate }} + } } @else { = + ['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({ + token: 'tb.calculated-field-func', + regex: `\\b${funcName}\\b`, + next: 'no_regex' + })); + const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = { calculatedFieldRollingArgumentValue: [ dotOperatorHighlightRule, @@ -368,6 +523,7 @@ const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = { regex: /timeWindow/, next: 'calculatedFieldRollingArgumentTimeWindow' }, + ...calculatedFieldRollingArgumentValueFunctionsHighlightRules, endGroupHighlightRule ], } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9de260bed2..1163024e90 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1069,7 +1069,8 @@ "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", "argument-type-required": "Argument type is required.", - "max-args": "Maximum number of arguments reached." + "max-args": "Maximum number of arguments reached.", + "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius." } }, "confirm-on-exit": { From 7dcddb9ffaf2a20996ad04cc3abaf25a596cbf20 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 15:18:13 +0200 Subject: [PATCH 37/42] Changed test expression help id --- .../calculated-field-script-test-dialog.component.html | 2 +- .../assets/help/en_US/calculated-field/test-expression_fn.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html index 57234aa692..cda99f6c09 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component.html @@ -45,7 +45,7 @@ [scriptLanguage]="ScriptLanguage.TBEL" [editorCompleter]="data.argumentsEditorCompleter" resultType="object" - helpId="calculated-field/test-expression_fn" + helpId="calculated-field/expression_fn" />
diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md deleted file mode 100644 index f8173dc528..0000000000 --- a/ui-ngx/src/assets/help/en_US/calculated-field/test-expression_fn.md +++ /dev/null @@ -1 +0,0 @@ - From 4620261e6fd52f706834ffa8df011436455d1e60 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 28 Feb 2025 15:27:22 +0200 Subject: [PATCH 38/42] refactoring --- .../components/dialog/calculated-field-dialog.component.html | 5 ++--- .../components/dialog/calculated-field-dialog.component.scss | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 7de7bce749..7e0f87a12e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -89,11 +89,10 @@ {{ 'calculated-fields.hint.expression-max-length' | translate }} } + } @else { + {{ 'calculated-fields.hint.expression' | translate }} } - @if (!configFormGroup.get('expressionSIMPLE').errors || !configFormGroup.get('expressionSIMPLE').touched) { - {{ 'calculated-fields.hint.expression' | translate }} - } } @else { Date: Fri, 28 Feb 2025 15:31:13 +0200 Subject: [PATCH 39/42] minor fixes --- .../server/dao/model/sql/CalculatedFieldLinkEntity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java index 3a529b7d93..0f2a6455ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java @@ -59,8 +59,7 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity Date: Fri, 28 Feb 2025 16:19:35 +0200 Subject: [PATCH 40/42] Use CLIENT_SCOPE by default for Push to cloud node --- .../main/data/json/edge/rule_chains/edge_root_rule_chain.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 6701b59e0e..ef0bf2698f 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -119,7 +119,7 @@ "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { - "scope": "SERVER_SCOPE" + "scope": "CLIENT_SCOPE" }, "externalId": null }, From b29cc4fc413a481ea6ce49b867a90d6aadfe8a51 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 11:20:04 +0200 Subject: [PATCH 41/42] Fix tupo --- .../org/thingsboard/server/queue/discovery/TopicService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 3e3051ee21..5992083d85 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -47,7 +47,7 @@ public class TopicService { @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated_fields.notifications-topic:calculated_field}") + @Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From bfef1a3ea3c7068c426ad3c69002c296532849d5 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 3 Mar 2025 11:21:50 +0200 Subject: [PATCH 42/42] Fix notifications_topic for cf --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 730dfec128..ec9925bf06 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1759,7 +1759,7 @@ queue: # Topic name for Calculated Field (CF) compacted states state_topic: "${TB_QUEUE_CF_STATE_TOPIC:tb_cf_state}" # For high-priority notifications that require minimum latency and processing time - notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field}" + notifications_topic: "${TB_QUEUE_CF_NOTIFICATIONS_TOPIC:calculated_field.notifications}" # Interval in milliseconds to poll messages by CF (Rule Engine) microservices poll_interval: "${TB_QUEUE_CF_POLL_INTERVAL_MS:25}" # Amount of partitions used by CF microservices