From f6ed8b2fdadea62c019de12b53515696df57c915 Mon Sep 17 00:00:00 2001 From: dpinkevych Date: Thu, 11 Jun 2026 08:45:20 +0300 Subject: [PATCH] avoid recursion to find relatedItems and change method names --- .../service/iot_hub/DefaultIotHubService.java | 57 +++++++++---------- .../iot-hub-install-dialog.component.html | 2 +- .../iot-hub-install-dialog.component.ts | 8 +-- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index ace31effbe..b7e4cb2a5d 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -683,39 +683,19 @@ public class DefaultIotHubService implements IotHubService { Set alreadyInstalledItemIds = new HashSet<>(iotHubInstalledItemService.findInstalledItemIdsByTenantId(tenantId)); - // LinkedHashMap preserves insertion order; we add deps before the root so a single - // forward iteration yields the correct topological install sequence. + // LinkedHashMap preserves insertion order; we add the related items (deps) before the + // root so a single forward iteration yields the correct install sequence (deps first, + // root last). Related items are leaf dependencies — IoT Hub items are only ever one + // level deep, so there is no need to walk a related item's own related items. LinkedHashMap entries = new LinkedHashMap<>(); - Set visiting = new HashSet<>(); - collectDependencies(rootVersion, alreadyInstalledItemIds, entries, visiting, true); - - return new InstallPlan(versionId, new ArrayList<>(entries.values())); - } - - /** - * Depth-first walk over {@code relatedItems}. Children are added BEFORE the current - * version so the resulting LinkedHashMap iterates deps-first, root-last. - */ - private void collectDependencies(JsonNode versionInfo, - Set alreadyInstalledItemIds, - LinkedHashMap entries, - Set visiting, - boolean root) { - String itemId = versionInfo.get("itemId").asText(); - if (entries.containsKey(itemId)) { - return; - } - if (!visiting.add(itemId)) { - log.warn("Dependency cycle detected involving IoT Hub item {} — breaking", itemId); - return; - } - - JsonNode related = versionInfo.get("relatedItems"); + String rootItemId = rootVersion.get("itemId").asText(); + JsonNode related = rootVersion.get("relatedItems"); if (related != null && related.isArray()) { for (JsonNode relatedNode : related) { String relatedItemId = relatedNode.asText(); - if (relatedItemId == null || relatedItemId.isEmpty() || entries.containsKey(relatedItemId)) { + if (relatedItemId == null || relatedItemId.isEmpty() + || relatedItemId.equals(rootItemId) || entries.containsKey(relatedItemId)) { continue; } JsonNode relatedVersion; @@ -731,11 +711,28 @@ public class DefaultIotHubService implements IotHubService { entries.put(relatedItemId, missingEntry(relatedItemId, "Item not found or not published")); continue; } - collectDependencies(relatedVersion, alreadyInstalledItemIds, entries, visiting, false); + addPlanEntry(relatedVersion, alreadyInstalledItemIds, entries, false); } } - visiting.remove(itemId); + addPlanEntry(rootVersion, alreadyInstalledItemIds, entries, true); + + return new InstallPlan(versionId, new ArrayList<>(entries.values())); + } + + /** + * Build an {@link InstallPlanEntry} for a single marketplace version and append it to the + * plan. No traversal of {@code relatedItems} happens here — related items are resolved one + * level deep by {@link #resolveInstallPlan}. + */ + private void addPlanEntry(JsonNode versionInfo, + Set alreadyInstalledItemIds, + LinkedHashMap entries, + boolean root) { + String itemId = versionInfo.get("itemId").asText(); + if (entries.containsKey(itemId)) { + return; + } InstallPlanEntry entry = new InstallPlanEntry(); entry.setItemId(itemId); diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 3f2b79fbfc..354399e5c8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -197,7 +197,7 @@ } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index 9c64353ef3..ccd8731ec8 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -259,9 +259,9 @@ export class TbIotHubInstallDialogComponent extends DialogComponent 0 || summary.missing > 0; - if (!hasDependencies && summary.willInstall === 1) { + if (!hasDependencies) { // Single root version, no deps, nothing to skip — install directly without showing the plan. - this.legacyInstall(); + this.installItem(); return; } @@ -277,7 +277,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent