From b84845f2588c70937ef619178ff0addf38fdd901 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 24 Apr 2026 17:58:02 +0300 Subject: [PATCH] refactor(solutions): use User instead of SecurityUser in install context Propagate User through SolutionInstallContext, DefaultSolutionService, and TbCalculatedFieldService.delete so non-security-context callers can drive solution installs and calculated-field cleanup. Pass TenantSolutionTemplateInstructions into the context constructor so the caller controls instruction state. --- .../cf/DefaultTbCalculatedFieldService.java | 3 +- .../entitiy/cf/TbCalculatedFieldService.java | 3 +- .../solutions/DefaultSolutionService.java | 1437 +++++++++-------- .../data/SolutionInstallContext.java | 7 +- 4 files changed, 777 insertions(+), 673 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index fde88bf084..7a7f907aa6 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -76,7 +77,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp @Override @Transactional - public void delete(CalculatedField calculatedField, SecurityUser user) { + public void delete(CalculatedField calculatedField, User user) { ActionType actionType = ActionType.DELETED; TenantId tenantId = calculatedField.getTenantId(); CalculatedFieldId calculatedFieldId = calculatedField.getId(); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 25b58c1d39..4cf12daae8 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.entitiy.cf; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -31,6 +32,6 @@ public interface TbCalculatedFieldService { PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink); - void delete(CalculatedField calculatedField, SecurityUser user); + void delete(CalculatedField calculatedField, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 1835ddbfde..882400b258 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -32,15 +32,19 @@ import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.Device; @@ -77,7 +81,6 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceConnectivityService; import org.thingsboard.server.dao.device.DockerComposeParams; -import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; @@ -210,28 +213,136 @@ public class DefaultSolutionService implements SolutionService { public SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception { Path tempDir = Files.createTempDirectory("iot-hub-solution-"); try { - extractZip(zipData, tempDir); - } catch (Throwable e) { - log.error("[{}] Failed to extract solution template zip", tenantId, e); + try { + extractZip(zipData, tempDir); + } catch (Throwable e) { + log.error("[{}] Failed to extract solution template zip", tenantId, e); + deleteDirectory(tempDir); + TenantSolutionTemplateInstructions instructions = new TenantSolutionTemplateInstructions(); + instructions.setDetails(e.getMessage()); + return new SolutionInstallResponse(instructions, false, List.of()); + } + + String solutionId = loadSolutionId(tempDir); + if (solutionId == null) { + throw new IllegalArgumentException("Solution template is missing solution.json or its 'title' field"); + } + + SolutionInstallResponse validateResult = validateSolution(tenantId, tempDir); + if (validateResult != null && !validateResult.isSuccess()) { + return validateResult; + } + return doInstallSolution(user, tenantId, solutionId, tempDir, request); + } finally { deleteDirectory(tempDir); - TenantSolutionTemplateInstructions instructions = new TenantSolutionTemplateInstructions(); - instructions.setDetails(e.getMessage()); - return new SolutionInstallResponse(instructions, false, List.of()); } - SolutionInstallContext ctx = new SolutionInstallContext(tenantId, loadSolutionId(tempDir), tempDir, user); + } + + @Override + public void deleteSolution(TenantId tenantId, List createdEntityIds, SecurityUser user) { + if (createdEntityIds == null || createdEntityIds.isEmpty()) { + return; + } + List entityIds = new ArrayList<>(createdEntityIds); + // Delete in the descending order of creation to avoid dependency issues. + Collections.reverse(entityIds); + for (EntityId entityId : entityIds) { + try { + deleteEntity(tenantId, entityId, user); + } catch (RuntimeException e) { + log.error("[{}] Failed to delete the entity: {}", tenantId, entityId, e); + } + } + } + + private SolutionInstallResponse validateSolution(TenantId tenantId, Path tempDir) { + Map> alreadyExistingEntities = new HashMap<>(); + + //TODO: check other entities. + + List ruleChains = loadListOfEntitiesIfFileExists(tempDir, "rule_chains.json", new TypeReference<>() { + }); + if (!ruleChains.isEmpty()) { + for (ReferenceableEntityDefinition ruleChain : ruleChains) { + List savedRuleChains = ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, new PageLink(1, 0, ruleChain.getName())).getData(); + if (savedRuleChains != null && !savedRuleChains.isEmpty()) { + alreadyExistingEntities.computeIfAbsent(EntityType.RULE_CHAIN, key -> new ArrayList<>()).add(savedRuleChains.get(0)); + } + } + } + + List deviceProfiles = loadListOfEntitiesIfFileExists(tempDir, "device_profiles.json", new TypeReference<>() { + }); + deviceProfiles.addAll(loadListOfEntitiesFromDirectory(tempDir, "device_profiles", DeviceProfileDefinition.class)); + // Validate that entities with such name does not exist entities + if (!deviceProfiles.isEmpty()) { + for (DeviceProfile deviceProfile : deviceProfiles) { + DeviceProfile savedProfile = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfile.getName()); + if (savedProfile != null) { + alreadyExistingEntities.computeIfAbsent(EntityType.DEVICE_PROFILE, key -> new ArrayList<>()).add(savedProfile); + } + } + } + + List assetProfiles = loadListOfEntitiesIfFileExists(tempDir, "asset_profiles.json", new TypeReference<>() { + }); + assetProfiles.addAll(loadListOfEntitiesFromDirectory(tempDir, "asset_profiles", AssetProfileDefinition.class)); + // Validate that entities with such name does not exist entities + if (!assetProfiles.isEmpty()) { + for (AssetProfile assetProfile : assetProfiles) { + AssetProfile savedProfile = assetProfileService.findAssetProfileByName(tenantId, assetProfile.getName()); + if (savedProfile != null) { + alreadyExistingEntities.computeIfAbsent(EntityType.ASSET_PROFILE, key -> new ArrayList<>()).add(savedProfile); + } + } + } + + List dashboards = loadListOfEntitiesIfFileExists(tempDir, "dashboards.json", new TypeReference<>() { + }); + if (!dashboards.isEmpty()) { + for (DashboardDefinition dashboard : dashboards) { + List savedDashboards = dashboardService.findDashboardsByTenantId(tenantId, new PageLink(1, 0, dashboard.getName())).getData(); + if (savedDashboards != null && !savedDashboards.isEmpty()) { + alreadyExistingEntities.computeIfAbsent(EntityType.DASHBOARD, key -> new ArrayList<>()).add(savedDashboards.get(0)); + } + } + } + if (!alreadyExistingEntities.isEmpty()) { + SolutionInstallResponse solutionInstallResponse = new SolutionInstallResponse(); + StringBuilder detailsBuilder = new StringBuilder(); + detailsBuilder.append("## Validation failed").append(System.lineSeparator()).append(System.lineSeparator()); + alreadyExistingEntities.forEach((type, list) -> detailsBuilder.append("The following **").append(getTypeLabel(type)).append("** entities already exist: ") + .append(list.stream().map(HasName::getName).map(name -> "'" + name + "'").collect(Collectors.joining(","))).append(";") + .append(System.lineSeparator()).append(System.lineSeparator())); + solutionInstallResponse.setSuccess(false); + solutionInstallResponse.setDetails(detailsBuilder.toString()); + return solutionInstallResponse; + } else { + return null; + } + } + + private SolutionInstallResponse doInstallSolution(User user, TenantId tenantId, String solutionId, Path tempDir, HttpServletRequest request) { + SolutionInstallContext ctx = new SolutionInstallContext(tenantId, solutionId, tempDir, user, new TenantSolutionTemplateInstructions()); + try { + registerEmulatorsAndComputeOldestTelemetryTs(ctx); provisionTenantDetails(ctx); provisionRuleChains(ctx); + provisionDeviceProfiles(ctx); + provisionAssetProfiles(ctx); List customers = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "customers.json", new TypeReference<>() {}); + provisionCustomers(ctx, customers); var assets = provisionAssets(ctx); + var devices = provisionDevices(ctx); provisionDashboards(ctx); @@ -239,7 +350,9 @@ public class DefaultSolutionService implements SolutionService { provisionCustomerUsers(ctx, customers); provisionRelations(ctx); + updateRuleChains(ctx); + provisionEdges(ctx); provisionAlarmRules(ctx); @@ -255,7 +368,9 @@ public class DefaultSolutionService implements SolutionService { List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); if (ruleChainDefs.stream().anyMatch(r -> StringUtils.isNotEmpty(r.getUpdate()))) { long timeout = loadInstallTimeoutMs(ctx.getTempDir()); - Thread.sleep(timeout); + if (timeout > 0) { + Thread.sleep(timeout); + } finalUpdateRuleChains(ctx); } @@ -267,154 +382,365 @@ public class DefaultSolutionService implements SolutionService { ctx.getCreatedEntitiesList() ); } catch (Throwable e) { - log.error("[{}] Failed to install solution template", tenantId, e); - rollback(tenantId, ctx, e); + log.error("[{}][{}] Failed to install solution template", tenantId, solutionId, e); + rollback(tenantId, solutionId, ctx, e); return new SolutionInstallResponse( new TenantSolutionTemplateInstructions(ctx.getSolutionInstructions()), false, ctx.getCreatedEntitiesList() ); - } finally { - deleteDirectory(tempDir); } } - @Override - public void deleteSolution(TenantId tenantId, List createdEntityIds, SecurityUser user) { - if (createdEntityIds == null || createdEntityIds.isEmpty()) { - return; + private void waitForTelemetryCompletion(Set> futures) throws InterruptedException { + CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + try { + all.get(); + Thread.sleep(futures.size() * 100L); + } catch (ExecutionException e) { + throw new RuntimeException("Telemetry processing failed", e.getCause()); } - List reversed = new ArrayList<>(createdEntityIds); - Collections.reverse(reversed); - for (EntityId entityId : reversed) { + } + + private void rollback(TenantId tenantId, String solutionId, SolutionInstallContext ctx, Throwable e) { + List createdEntities = new ArrayList<>(ctx.getCreatedEntitiesList()); + Collections.reverse(createdEntities); + for (EntityId entityId : createdEntities) { try { - deleteEntity(tenantId, entityId, user); - } catch (Exception e) { - log.error("[{}] Failed to delete entity: {}", tenantId, entityId, e); + deleteEntity(tenantId, entityId, ctx.getUser()); + } catch (RuntimeException re) { + log.error("[{}][{}] Failed to delete the entity: {}", tenantId, solutionId, entityId, re); } } + ctx.getCreatedEntitiesList().clear(); + ctx.getSolutionInstructions().setDetails(e.getMessage()); } - private void provisionRuleChains(SolutionInstallContext ctx) { - List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); - for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { - Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); - if (!Files.exists(ruleChainPath)) { - log.warn("[{}] Rule chain file not found: {}", ctx.getTenantId(), entityDef.getFile()); - continue; - } - JsonNode ruleChainJson = replaceIds(ctx, JacksonUtil.toJsonNode(ruleChainPath)); + private String prepareInstructions(SolutionInstallContext ctx, HttpServletRequest request) { - RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - ruleChain.setId(null); - ruleChain.setTenantId(ctx.getTenantId()); - String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); - RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + Path instructionsFile = ctx.getTempDir().resolve("instructions.md"); + if (!Files.exists(instructionsFile)) { + return null; + } + String template; + try { + template = Files.readString(instructionsFile); + } catch (IOException e) { + log.warn("[{}] Failed to read instructions.md", ctx.getTenantId(), e); + return null; + } - RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); - metadata.setRuleChainId(savedRuleChain.getId()); - metadata.setVersion(savedRuleChain.getVersion()); - ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); - if (ruleChain.isRoot()) { - ruleChainService.setRootRuleChain(ctx.getTenantId(), savedRuleChain.getId()); - } + String baseUrl = systemSecurityService.getBaseUrl(ctx.getTenantId(), null, request); - ctx.register(entityDef.getJsonId(), savedRuleChain); - log.debug("[{}] Rule chain provisioned: {}", ctx.getTenantId(), savedRuleChain.getName()); + // Inject edge instructions first, then run the full replacement logic on the combined string + if (template.contains("${edge_instructions}")) { + if (ctx.getCreatedEdges().isEmpty()) { + template = template.replace("${edge_instructions}", ""); + } else { + Path edgeFile = ctx.getTempDir().resolve("edge_instructions.md"); + String edgeTemplate = Files.exists(edgeFile) ? readFileContent(edgeFile) : ""; + template = template.replace("${edge_instructions}", edgeTemplate); + } } - } - private void updateRuleChains(SolutionInstallContext ctx) { - List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); - for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { - Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); - if (!Files.exists(ruleChainPath)) { - continue; - } - String realId = ctx.getRealIds().get(entityDef.getJsonId()); - if (realId == null) { - continue; - } - RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realId)); - RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); - if (savedRuleChain == null) { - continue; - } - JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); - String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); - String oldMetadataStr = metadataStr; - for (var entry : ctx.getRealIds().entrySet()) { - metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); - } - if (metadataStr.equals(oldMetadataStr)) { - continue; + template = template.replace("${DOCS_BASE_URL}", docsBaseUrl); + template = template.replace("${BASE_URL}", baseUrl); + + TenantSolutionTemplateInstructions solutionInstructions = ctx.getSolutionInstructions(); + + if (solutionInstructions.getDashboardId() != null) { + template = template.replace("${MAIN_DASHBOARD_URL}", + getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), false)); + if (solutionInstructions.isMainDashboardPublic()) { + template = template.replace("${MAIN_DASHBOARD_PUBLIC_URL}", + getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), true)); } - RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); - metadata.setRuleChainId(ruleChainId); - metadata.setVersion(savedRuleChain.getVersion()); - ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); } - } - private void finalUpdateRuleChains(SolutionInstallContext ctx) { - List ruleChains = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); - for (ReferenceableEntityDefinition entityDefinition : ruleChains) { - if (StringUtils.isEmpty(entityDefinition.getUpdate())) { - continue; - } - Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDefinition.getUpdate()); - JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); - RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); - ruleChain.setTenantId(ctx.getTenantId()); - String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); - for (var entry : ctx.getRealIds().entrySet()) { - metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); + for (DashboardLinkInfo dashboardLinkInfo : ctx.getDashboardLinks()) { + template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_URL}", + getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), false)); + if (dashboardLinkInfo.isPublic()) { + template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_PUBLIC_URL}", + getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), true)); } - RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + } - String realRuleChainId = ctx.getRealIds().get(entityDefinition.getJsonId()); - if (StringUtils.isEmpty(realRuleChainId)) { - continue; - } - RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realRuleChainId)); - RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); - ruleChainMetaData.setRuleChainId(savedRuleChain.getId()); - ruleChainMetaData.setVersion(savedRuleChain.getVersion()); - ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), ruleChainMetaData, tbRuleChainService::updateRuleNodeConfiguration); + if (template.contains("${GATEWAYS_URL}")) { + template = template.replace("${GATEWAYS_URL}", "/gateways"); } - } - private String loadSolutionId(Path tempDir) { - Path solutionJson = tempDir.resolve("solution.json"); - if (Files.exists(solutionJson)) { - JsonNode node = JacksonUtil.toJsonNode(solutionJson); - if (node != null && node.has("title")) { - String title = node.get("title").asText(""); - return title.trim().toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); + // Device list and credentials + StringBuilder devList = new StringBuilder(); + devList.append("| Device name | Access token | Owner |"); + devList.append(System.lineSeparator()); + devList.append("| :--- | :--- | :--- |"); + devList.append(System.lineSeparator()); + + for (DeviceCredentialsInfo credentialsInfo : ctx.getCreatedDevices().values()) { + devList.append("|").append(credentialsInfo.getName()) + .append("|").append(credentialsInfo.getCredentials().getCredentialsId()).append("{:copy-code}") + .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : "Tenant"); + devList.append(System.lineSeparator()); + + template = template.replace("${" + credentialsInfo.getName() + "ACCESS_TOKEN}", credentialsInfo.getCredentials().getCredentialsId()); + + if (credentialsInfo.isGateway()) { + template = template.replace("${DOCKER_CONFIG}", + prepareDockerComposeFile(ctx.getTenantId(), ctx.getSolutionId(), baseUrl, credentialsInfo.getCredentials().getDeviceId())); } } - return null; - } - private long loadInstallTimeoutMs(Path tempDir) { - Path solutionJson = tempDir.resolve("solution.json"); - if (Files.exists(solutionJson)) { - JsonNode node = JacksonUtil.toJsonNode(solutionJson); - if (node != null && node.has("installTimeoutMs")) { - return node.get("installTimeoutMs").asLong(0L); - } + template = template.replace("${device_list_and_credentials}", devList.toString()); + + // User list (without user group column) + StringBuilder userList = new StringBuilder(); + userList.append("| Name | Login | Password | Customer name |"); + userList.append(System.lineSeparator()); + userList.append("| :--- | :--- | :--- | :--- |"); + userList.append(System.lineSeparator()); + + for (UserCredentialsInfo credentialsInfo : ctx.getCreatedUsers().values()) { + userList.append("|").append(credentialsInfo.getName()) + .append("|").append(credentialsInfo.getLogin()).append("{:copy-code}") + .append("|").append(credentialsInfo.getPassword()).append("{:copy-code}") + .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : ""); + userList.append(System.lineSeparator()); } - return 0L; - } - private void provisionDeviceProfiles(SolutionInstallContext ctx) { - List deviceProfiles = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_profiles.json", new TypeReference<>() {}); - deviceProfiles.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "device_profiles", DeviceProfileDefinition.class)); - deviceProfiles.forEach(deviceProfile -> { - deviceProfile.setId(null); - deviceProfile.setCreatedTime(0L); - deviceProfile.setTenantId(ctx.getTenantId()); - if (deviceProfile.getDefaultRuleChainId() != null) { + template = template.replace("${user_list}", userList.toString()); + + // Edge detail URLs + for (Map.Entry edgeLinkInfoEntry : ctx.getCreatedEdges().entrySet()) { + EdgeLinkInfo edgeLinkInfo = edgeLinkInfoEntry.getValue(); + StringBuilder edgeDetailsUrl = new StringBuilder(); + if (EntityType.CUSTOMER.equals(edgeLinkInfo.getOwnerId().getEntityType())) { + edgeDetailsUrl.append("/customers/").append(edgeLinkInfo.getOwnerId().getId()); + edgeDetailsUrl.append("/edgeInstances/").append(edgeLinkInfo.getEdgeId().getId()); + } else { + edgeDetailsUrl.append("/edgeManagement/instances/").append(edgeLinkInfo.getEdgeId().getId()); + } + String edgeName = edgeLinkInfoEntry.getKey(); + String edgeDetailsPlaceholder = "${" + edgeName + "EDGE_DETAILS_URL}"; + template = template.replace(edgeDetailsPlaceholder, edgeDetailsUrl.toString()); + } + + template = replaceAlarmRules(ctx, template); + template = replaceCalculatedFields(ctx, template); + template = replaceCreatedEntities(ctx, template); + + return template; + } + + private static String replaceAlarmRules(SolutionInstallContext ctx, String template) { + StringBuilder alarmRules = new StringBuilder(); + + alarmRules.append("| Entity Profile Name | Alarm Type | Severities |").append(System.lineSeparator()); + alarmRules.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + ctx.getCreatedAlarmRules().entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.comparing(CreatedAlarmRuleInfo::entityName, String.CASE_INSENSITIVE_ORDER) + .thenComparing(CreatedAlarmRuleInfo::alarmType, String.CASE_INSENSITIVE_ORDER))) + .forEach(entry -> { + UUID key = entry.getKey(); + var alarmRuleInfo = entry.getValue(); + + String alarmType = alarmRuleInfo.alarmType(); + String link = alarmRuleInfo.getCfPageLink(key); + + String alarmTypeWithLink = "" + alarmType + ""; + + String profileName = alarmRuleInfo.entityId() != null ? + "" + alarmRuleInfo.entityName() + "" + : alarmRuleInfo.entityName(); + + alarmRules.append("|") + .append(profileName).append("|") + .append(alarmTypeWithLink).append("|") + .append(alarmRuleInfo.severities()).append("|") + .append(System.lineSeparator()); + }); + + return template.replace("${alarm_rules}", alarmRules.toString()); + } + + private static String replaceCalculatedFields(SolutionInstallContext ctx, String template) { + StringBuilder calculatedFields = new StringBuilder(); + + calculatedFields.append("| Entity Profile Name | Field Name | Field Type |").append(System.lineSeparator()); + calculatedFields.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + ctx.getCreatedCalculatedFields().entrySet().stream() + .sorted(Map.Entry.comparingByValue( + Comparator.comparing(CreatedCalculatedFieldInfo::entityName, String.CASE_INSENSITIVE_ORDER) + .thenComparing(CreatedCalculatedFieldInfo::name, String.CASE_INSENSITIVE_ORDER) + )) + .forEach(entry -> { + UUID key = entry.getKey(); + var cfInfo = entry.getValue(); + + String cfTitle = cfInfo.name(); + String link = cfInfo.getCfPageLink(key); + + String cfTitleWithLink = "" + cfTitle + ""; + + String profileName = cfInfo.entityId() != null ? + "" + cfInfo.entityName() + "" + : cfInfo.entityName(); + + calculatedFields.append("|") + .append(profileName).append("|") + .append(cfTitleWithLink).append("|") + .append(cfInfo.type()).append("|") + .append(System.lineSeparator()); + }); + + return template.replace("${calculated_fields}", calculatedFields.toString()); + } + + private static String replaceCreatedEntities(SolutionInstallContext ctx, String template) { + StringBuilder entityList = new StringBuilder(); + + entityList.append("| Name | Type | Owner |").append(System.lineSeparator()); + entityList.append("| :--- | :--- | :--- |").append(System.lineSeparator()); + + for (Map.Entry entry : ctx.getCreatedEntities().entrySet()) { + UUID key = entry.getKey(); + var entityInfo = entry.getValue(); + String link = entityInfo.getEntityPageLink(key); + String entityName = entityInfo.getName(); + + String name = link != null ? + "" + entityName + "" + : entityName; + + entityList.append("|") + .append(name).append("|") + .append(entityInfo.getType().getNormalName()).append("|") + .append(entityInfo.getOwner()).append("|") + .append(System.lineSeparator()); + } + return template.replace("${all_entities}", entityList.toString()); + } + + private String getDashboardLink(TenantSolutionTemplateInstructions solutionInstructions, DashboardId dashboardId, boolean isPublic) { + if (isPublic && solutionInstructions.getPublicId() != null) { + return "/dashboard/" + dashboardId.getId() + "?publicId=" + solutionInstructions.getPublicId(); + } + return "/dashboards/" + dashboardId.getId(); + } + + private String prepareDockerComposeFile(TenantId tenantId, String solutionId, String baseUrl, DeviceId deviceId) { + Device device = new Device(deviceId); + device.setTenantId(tenantId); + String containerName = "tb-gateway-" + solutionId.replace('_', '-'); + DockerComposeParams params = new DockerComposeParams(false, containerName, false, true, false, false); + try (InputStream inputStream = deviceConnectivityService.createGatewayDockerComposeFile(baseUrl, device, params).getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + ) { + return reader.lines().collect(Collectors.joining("\n")); + } catch (Exception e) { + throw new RuntimeException("Failed to read or process the docker-compose.yml file.", e); + } + } + + private void provisionRuleChains(SolutionInstallContext ctx) { + List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); + if (!Files.exists(ruleChainPath)) { + log.warn("[{}] Rule chain file not found: {}", ctx.getTenantId(), entityDef.getFile()); + continue; + } + JsonNode ruleChainJson = replaceIds(ctx, JacksonUtil.toJsonNode(ruleChainPath)); + + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + ruleChain.setId(null); + ruleChain.setTenantId(ctx.getTenantId()); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + + RuleChain savedRuleChain = ruleChainService.saveRuleChain(ruleChain); + metadata.setRuleChainId(savedRuleChain.getId()); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); + if (ruleChain.isRoot()) { + ruleChainService.setRootRuleChain(ctx.getTenantId(), savedRuleChain.getId()); + } + + ctx.register(entityDef.getJsonId(), savedRuleChain); + log.debug("[{}] Rule chain provisioned: {}", ctx.getTenantId(), savedRuleChain.getName()); + } + } + + private void updateRuleChains(SolutionInstallContext ctx) { + List ruleChainDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDef : ruleChainDefs) { + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDef.getFile()); + if (!Files.exists(ruleChainPath)) { + continue; + } + String realId = ctx.getRealIds().get(entityDef.getJsonId()); + if (realId == null) { + continue; + } + RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realId)); + RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); + if (savedRuleChain == null) { + continue; + } + JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + String oldMetadataStr = metadataStr; + for (var entry : ctx.getRealIds().entrySet()) { + metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); + } + if (metadataStr.equals(oldMetadataStr)) { + continue; + } + RuleChainMetaData metadata = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + metadata.setRuleChainId(ruleChainId); + metadata.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), metadata, tbRuleChainService::updateRuleNodeConfiguration); + } + } + + private void finalUpdateRuleChains(SolutionInstallContext ctx) { + List ruleChains = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "rule_chains.json", new TypeReference<>() {}); + for (ReferenceableEntityDefinition entityDefinition : ruleChains) { + if (StringUtils.isEmpty(entityDefinition.getUpdate())) { + continue; + } + Path ruleChainPath = ctx.getTempDir().resolve("rule_chains").resolve(entityDefinition.getUpdate()); + JsonNode ruleChainJson = JacksonUtil.toJsonNode(ruleChainPath); + RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class); + ruleChain.setTenantId(ctx.getTenantId()); + String metadataStr = JacksonUtil.toString(ruleChainJson.get("metadata")); + for (var entry : ctx.getRealIds().entrySet()) { + metadataStr = metadataStr.replace(entry.getKey(), entry.getValue()); + } + RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(JacksonUtil.toJsonNode(metadataStr), RuleChainMetaData.class); + + String realRuleChainId = ctx.getRealIds().get(entityDefinition.getJsonId()); + if (StringUtils.isEmpty(realRuleChainId)) { + continue; + } + RuleChainId ruleChainId = new RuleChainId(UUID.fromString(realRuleChainId)); + RuleChain savedRuleChain = ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChainId); + ruleChainMetaData.setRuleChainId(savedRuleChain.getId()); + ruleChainMetaData.setVersion(savedRuleChain.getVersion()); + ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), ruleChainMetaData, tbRuleChainService::updateRuleNodeConfiguration); + } + } + + private void provisionDeviceProfiles(SolutionInstallContext ctx) { + List deviceProfiles = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_profiles.json", new TypeReference<>() {}); + deviceProfiles.addAll(loadListOfEntitiesFromDirectory(ctx.getTempDir(), "device_profiles", DeviceProfileDefinition.class)); + deviceProfiles.forEach(deviceProfile -> { + deviceProfile.setId(null); + deviceProfile.setCreatedTime(0L); + deviceProfile.setTenantId(ctx.getTenantId()); + if (deviceProfile.getDefaultRuleChainId() != null) { String newId = ctx.getRealIds().get(deviceProfile.getDefaultRuleChainId().getId().toString()); if (newId != null) { deviceProfile.setDefaultRuleChainId(new RuleChainId(UUID.fromString(newId))); @@ -479,46 +805,6 @@ public class DefaultSolutionService implements SolutionService { }); } - private Map provisionAssets(SolutionInstallContext ctx) { - Map result = new HashMap<>(); - Set assetTypeSet = new HashSet<>(); - List assets = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "assets.json", new TypeReference<>() {}); - for (AssetDefinition entityDef : assets) { - Asset entity = new Asset(); - entity.setTenantId(ctx.getTenantId()); - entity.setName(entityDef.getName()); - entity.setLabel(entityDef.getLabel()); - entity.setType(entityDef.getType()); - if (entityDef.isMakePublic()) { - entity.setCustomerId(getPublicCustomerId(ctx)); - } else { - entity.setCustomerId(ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer())); - } - ensureAssetProfileExists(ctx, assetTypeSet, entityDef); - entity = assetService.saveAsset(entity); - ctx.register(entityDef, entity); - log.info("[{}] Saved asset: {}", entity.getId(), entity); - AssetId entityId = entity.getId(); - ctx.putIdToMap(entityDef, entityId); - saveServerSideAttributes(ctx, entityId, entityDef.getAttributes()); - ctx.put(entityId, entityDef.getRelations()); - result.put(entity, entityDef); - } - return result; - } - - private void ensureAssetProfileExists(SolutionInstallContext ctx, Set assetTypeSet, AssetDefinition entityDef) { - if (!assetTypeSet.contains(entityDef.getType())) { - AssetProfile assetProfile = assetProfileService.findAssetProfileByName(ctx.getTenantId(), entityDef.getType()); - if (assetProfile == null) { - AssetProfile created = assetProfileService.findOrCreateAssetProfile(ctx.getTenantId(), entityDef.getType()); - ctx.register(created.getId()); - log.info("Saved asset profile: {}", created.getId()); - } - assetTypeSet.add(entityDef.getType()); - } - } - private CustomerId getPublicCustomerId(SolutionInstallContext ctx) { CustomerId publicId = ctx.getSolutionInstructions().getPublicId(); if (publicId != null) { @@ -529,6 +815,79 @@ public class DefaultSolutionService implements SolutionService { return publicCustomer.getId(); } + private void provisionDashboards(SolutionInstallContext ctx) { + List dashboardDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "dashboards.json", new TypeReference<>() {}); + for (DashboardDefinition entityDef : dashboardDefs) { + CustomerId customerId = entityDef.isMakePublic() ? getPublicCustomerId(ctx) : ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer()); + Path dashboardPath = ctx.getTempDir().resolve("dashboards").resolve(entityDef.getFile()); + if (!Files.exists(dashboardPath)) { + log.warn("[{}] Dashboard file not found: {}", ctx.getTenantId(), entityDef.getFile()); + continue; + } + JsonNode dashboardJson = replaceIds(ctx, JacksonUtil.toJsonNode(dashboardPath)); + Dashboard dashboardTemplate = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); + + Dashboard dashboard = new Dashboard(); + dashboard.setTenantId(ctx.getTenantId()); + dashboard.setTitle(entityDef.getName()); + dashboard.setConfiguration(dashboardTemplate.getConfiguration()); + dashboard.setImage(dashboardTemplate.getImage()); + dashboard.setResources(dashboardTemplate.getResources()); + if (dashboardJson.has("mobileHide") && dashboardJson.get("mobileHide").isBoolean()) { + dashboard.setMobileHide(dashboardJson.get("mobileHide").asBoolean()); + } + if (dashboardJson.has("mobileOrder") && dashboardJson.get("mobileOrder").isInt()) { + dashboard.setMobileOrder(dashboardJson.get("mobileOrder").asInt()); + } + + dashboard = dashboardService.saveDashboard(dashboard); + if (customerId != null) { + dashboardService.assignDashboardToCustomer(ctx.getTenantId(), dashboard.getId(), customerId); + } + ctx.register(entityDef, dashboard); + ctx.putIdToMap(EntityType.DASHBOARD, entityDef.getName(), dashboard.getId()); + + if (entityDef.isMain()) { + ctx.getSolutionInstructions().setDashboardId(dashboard.getId()); + ctx.getSolutionInstructions().setMainDashboardPublic(entityDef.isMakePublic()); + } + ctx.getDashboardLinks().add(new DashboardLinkInfo(dashboard.getTitle(), dashboard.getId(), entityDef.isMakePublic())); + + log.debug("[{}] Dashboard provisioned: {}", ctx.getTenantId(), dashboard.getTitle()); + } + } + + private void provisionRelations(SolutionInstallContext ctx) { + ctx.getRelationDefinitions().forEach((id, relations) -> { + for (RelationDefinition relationDef : relations) { + log.info("[{}] Saving relation: {}", id, relationDef); + EntityRelation entityRelation = new EntityRelation(); + EntityId otherId = resolveRelatedEntityId(relationDef, ctx); + if (EntitySearchDirection.FROM.equals(relationDef.getDirection())) { + entityRelation.setFrom(otherId); + entityRelation.setTo(id); + } else { + entityRelation.setFrom(id); + entityRelation.setTo(otherId); + } + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + entityRelation.setType(relationDef.getType()); + try { + relationService.save(ctx.getTenantId(), null, entityRelation, null); + } catch (Exception e) { + log.info("[{}] Failed to save relation: {}, cause: {}", id, relationDef, e.getMessage()); + } + } + }); + } + + private EntityId resolveRelatedEntityId(RelationDefinition relationDef, SolutionInstallContext ctx) { + if (relationDef.getEntityType() == EntityType.TENANT) { + return ctx.getTenantId(); + } + return ctx.getIdFromMap(relationDef.getEntityType(), relationDef.getEntityName()); + } + private Map provisionDevices(SolutionInstallContext ctx) { Map result = new HashMap<>(); Set deviceTypeSet = new HashSet<>(); @@ -580,6 +939,115 @@ public class DefaultSolutionService implements SolutionService { } } + private void registerEmulatorsAndComputeOldestTelemetryTs(SolutionInstallContext ctx) { + List emulatorDefinitions = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_emulators.json", new TypeReference<>() { + }); + Map deviceEmulators = emulatorDefinitions.stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); + emulatorDefinitions.stream().filter(ed -> StringUtils.isNotEmpty(ed.getExtendz())) + .forEach(ed -> { + EmulatorDefinition parent = deviceEmulators.get(ed.getExtendz()); + if (parent != null) { + ed.enrich(parent); + } + }); + Map assetEmulators = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "asset_emulators.json", new TypeReference>() { + }).stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); + + ctx.setDeviceEmulators(deviceEmulators); + ctx.setAssetEmulators(assetEmulators); + + long solutionInstallTs = ctx.getInstallTs(); + long oldestDeviceEmulatorsTs = deviceEmulators.values().stream() + .mapToLong(value -> value.getOldestTs(solutionInstallTs)) + .min().orElse(solutionInstallTs); + long oldestAssetEmulatorsTs = assetEmulators.values().stream() + .mapToLong(value -> value.getOldestTs(solutionInstallTs)) + .min().orElse(solutionInstallTs); + long solutionOldestTs = Math.min(oldestDeviceEmulatorsTs, oldestAssetEmulatorsTs); + + ctx.setOldestTelemetryTs(solutionOldestTs); + } + + private Set> launchEmulators(SolutionInstallContext ctx, Map devicesMap, Map assets) throws Exception { + Set> results = new HashSet<>(); + + for (var entry : devicesMap.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { + results.add(DeviceEmulatorLauncher.builder() + .entity(entry.getKey()) + .emulatorDefinition(ctx.getDeviceEmulators().get(entry.getValue().getEmulator())) + .oldTelemetryExecutor(emulatorExecutor) + .tbClusterService(tbClusterService) + .partitionService(partitionService) + .tbQueueProducerProvider(tbQueueProducerProvider) + .serviceInfoProvider(serviceInfoProvider) + .tsSubService(tsSubService) + .build().launch()); + } + + for (var entry : assets.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { + results.add(AssetEmulatorLauncher.builder() + .entity(entry.getKey()) + .emulatorDefinition(ctx.getAssetEmulators().get(entry.getValue().getEmulator())) + .oldTelemetryExecutor(emulatorExecutor) + .tbClusterService(tbClusterService) + .partitionService(partitionService) + .tbQueueProducerProvider(tbQueueProducerProvider) + .serviceInfoProvider(serviceInfoProvider) + .tsSubService(tsSubService) + .build().launch()); + } + + return results; + } + + private void provisionTenantDetails(SolutionInstallContext ctx) { + TenantDefinition tenant = loadEntityIfFileExists(ctx.getTempDir(), "tenant.json", TenantDefinition.class); + if (tenant != null) { + saveServerSideAttributes(ctx, ctx.getTenantId(), tenant.getAttributes()); + ctx.put(ctx.getTenantId(), tenant.getRelations()); + } + } + + private Map provisionAssets(SolutionInstallContext ctx) { + Map result = new HashMap<>(); + Set assetTypeSet = new HashSet<>(); + List assets = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "assets.json", new TypeReference<>() {}); + for (AssetDefinition entityDef : assets) { + Asset entity = new Asset(); + entity.setTenantId(ctx.getTenantId()); + entity.setName(entityDef.getName()); + entity.setLabel(entityDef.getLabel()); + entity.setType(entityDef.getType()); + if (entityDef.isMakePublic()) { + entity.setCustomerId(getPublicCustomerId(ctx)); + } else { + entity.setCustomerId(ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer())); + } + ensureAssetProfileExists(ctx, assetTypeSet, entityDef); + entity = assetService.saveAsset(entity); + ctx.register(entityDef, entity); + log.info("[{}] Saved asset: {}", entity.getId(), entity); + AssetId entityId = entity.getId(); + ctx.putIdToMap(entityDef, entityId); + saveServerSideAttributes(ctx, entityId, entityDef.getAttributes()); + ctx.put(entityId, entityDef.getRelations()); + result.put(entity, entityDef); + } + return result; + } + + private void ensureAssetProfileExists(SolutionInstallContext ctx, Set assetTypeSet, AssetDefinition entityDef) { + if (!assetTypeSet.contains(entityDef.getType())) { + AssetProfile assetProfile = assetProfileService.findAssetProfileByName(ctx.getTenantId(), entityDef.getType()); + if (assetProfile == null) { + AssetProfile created = assetProfileService.findOrCreateAssetProfile(ctx.getTenantId(), entityDef.getType()); + ctx.register(created.getId()); + log.info("Saved asset profile: {}", created.getId()); + } + assetTypeSet.add(entityDef.getType()); + } + } + private void provisionCustomers(SolutionInstallContext ctx, List customers) { for (CustomerDefinition entityDef : customers) { entityDef.setRandomNameData(generateRandomName(ctx)); @@ -814,403 +1282,45 @@ public class DefaultSolutionService implements SolutionService { } } - private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) { - cf.setId(null); - cf.setCreatedTime(0L); - cf.setTenantId(ctx.getTenantId()); - cf.setDebugSettings(new DebugSettings(true, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15))); - - Map realIds = ctx.getRealIds(); - - EntityId entityId = cf.getEntityId(); - if (entityId != null) { - String newEntityId = realIds.get(entityId.getId().toString()); - if (newEntityId != null) { - cf.setEntityId(EntityIdFactory.getByTypeAndUuid(entityId.getEntityType(), newEntityId)); - } else { - log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); - throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); - } - } - if (cf.getConfiguration() instanceof BaseCalculatedFieldConfiguration baseCfg) { - baseCfg.getArguments().forEach((key, argument) -> { - EntityId refEntityId = argument.getRefEntityId(); - if (refEntityId != null) { - if (refEntityId.getEntityType() == EntityType.TENANT) { - argument.setRefEntityId(ctx.getTenantId()); - } else { - String newId = realIds.get(refEntityId.getId().toString()); - if (newId != null) { - argument.setRefEntityId(EntityIdFactory.getByTypeAndUuid(refEntityId.getEntityType(), newId)); - } else { - log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); - throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); - } - } - } - }); - } - - CalculatedField calculatedField = new CalculatedField(cf); - return calculatedFieldService.save(calculatedField); - } - - private void provisionTenantDetails(SolutionInstallContext ctx) { - TenantDefinition tenant = loadEntityIfFileExists(ctx.getTempDir(), "tenant.json", TenantDefinition.class); - if (tenant != null) { - saveServerSideAttributes(ctx, ctx.getTenantId(), tenant.getAttributes()); - ctx.put(ctx.getTenantId(), tenant.getRelations()); - } - } - - private void registerEmulatorsAndComputeOldestTelemetryTs(SolutionInstallContext ctx) { - List emulatorDefinitions = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "device_emulators.json", new TypeReference<>() { - }); - Map deviceEmulators = emulatorDefinitions.stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); - emulatorDefinitions.stream().filter(ed -> StringUtils.isNotEmpty(ed.getExtendz())) - .forEach(ed -> { - EmulatorDefinition parent = deviceEmulators.get(ed.getExtendz()); - if (parent != null) { - ed.enrich(parent); - } - }); - Map assetEmulators = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "asset_emulators.json", new TypeReference>() { - }).stream().collect(Collectors.toMap(EmulatorDefinition::getName, Function.identity())); - - ctx.setDeviceEmulators(deviceEmulators); - ctx.setAssetEmulators(assetEmulators); - - long solutionInstallTs = ctx.getInstallTs(); - long oldestDeviceEmulatorsTs = deviceEmulators.values().stream() - .mapToLong(value -> value.getOldestTs(solutionInstallTs)) - .min().orElse(solutionInstallTs); - long oldestAssetEmulatorsTs = assetEmulators.values().stream() - .mapToLong(value -> value.getOldestTs(solutionInstallTs)) - .min().orElse(solutionInstallTs); - long solutionOldestTs = Math.min(oldestDeviceEmulatorsTs, oldestAssetEmulatorsTs); - - ctx.setOldestTelemetryTs(solutionOldestTs); - } - - private Set> launchEmulators(SolutionInstallContext ctx, Map devicesMap, Map assets) throws Exception { - Set> results = new HashSet<>(); - - for (var entry : devicesMap.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { - results.add(DeviceEmulatorLauncher.builder() - .entity(entry.getKey()) - .emulatorDefinition(ctx.getDeviceEmulators().get(entry.getValue().getEmulator())) - .oldTelemetryExecutor(emulatorExecutor) - .tbClusterService(tbClusterService) - .partitionService(partitionService) - .tbQueueProducerProvider(tbQueueProducerProvider) - .serviceInfoProvider(serviceInfoProvider) - .tsSubService(tsSubService) - .build().launch()); - } - - for (var entry : assets.entrySet().stream().filter(e -> StringUtils.isNotBlank(e.getValue().getEmulator())).collect(Collectors.toSet())) { - results.add(AssetEmulatorLauncher.builder() - .entity(entry.getKey()) - .emulatorDefinition(ctx.getAssetEmulators().get(entry.getValue().getEmulator())) - .oldTelemetryExecutor(emulatorExecutor) - .tbClusterService(tbClusterService) - .partitionService(partitionService) - .tbQueueProducerProvider(tbQueueProducerProvider) - .serviceInfoProvider(serviceInfoProvider) - .tsSubService(tsSubService) - .build().launch()); - } - - return results; - } - - private void waitForTelemetryCompletion(Set> futures) throws InterruptedException { - CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); - try { - all.get(); - Thread.sleep(futures.size() * 100L); - } catch (ExecutionException e) { - throw new RuntimeException("Telemetry processing failed", e.getCause()); - } - } - - private void provisionDashboards(SolutionInstallContext ctx) { - List dashboardDefs = loadListOfEntitiesIfFileExists(ctx.getTempDir(), "dashboards.json", new TypeReference<>() {}); - for (DashboardDefinition entityDef : dashboardDefs) { - CustomerId customerId = entityDef.isMakePublic() ? getPublicCustomerId(ctx) : ctx.getIdFromMap(EntityType.CUSTOMER, entityDef.getCustomer()); - Path dashboardPath = ctx.getTempDir().resolve("dashboards").resolve(entityDef.getFile()); - if (!Files.exists(dashboardPath)) { - log.warn("[{}] Dashboard file not found: {}", ctx.getTenantId(), entityDef.getFile()); - continue; - } - JsonNode dashboardJson = replaceIds(ctx, JacksonUtil.toJsonNode(dashboardPath)); - Dashboard dashboardTemplate = JacksonUtil.treeToValue(dashboardJson, Dashboard.class); - - Dashboard dashboard = new Dashboard(); - dashboard.setTenantId(ctx.getTenantId()); - dashboard.setTitle(entityDef.getName()); - dashboard.setConfiguration(dashboardTemplate.getConfiguration()); - dashboard.setImage(dashboardTemplate.getImage()); - dashboard.setResources(dashboardTemplate.getResources()); - if (dashboardJson.has("mobileHide") && dashboardJson.get("mobileHide").isBoolean()) { - dashboard.setMobileHide(dashboardJson.get("mobileHide").asBoolean()); - } - if (dashboardJson.has("mobileOrder") && dashboardJson.get("mobileOrder").isInt()) { - dashboard.setMobileOrder(dashboardJson.get("mobileOrder").asInt()); - } - - dashboard = dashboardService.saveDashboard(dashboard); - if (customerId != null) { - dashboardService.assignDashboardToCustomer(ctx.getTenantId(), dashboard.getId(), customerId); - } - ctx.register(entityDef, dashboard); - ctx.putIdToMap(EntityType.DASHBOARD, entityDef.getName(), dashboard.getId()); - - if (entityDef.isMain()) { - ctx.getSolutionInstructions().setDashboardId(dashboard.getId()); - ctx.getSolutionInstructions().setMainDashboardPublic(entityDef.isMakePublic()); - } - ctx.getDashboardLinks().add(new DashboardLinkInfo(dashboard.getTitle(), dashboard.getId(), entityDef.isMakePublic())); - - log.debug("[{}] Dashboard provisioned: {}", ctx.getTenantId(), dashboard.getTitle()); - } - } - - private void provisionRelations(SolutionInstallContext ctx) { - ctx.getRelationDefinitions().forEach((id, relations) -> { - for (RelationDefinition relationDef : relations) { - log.info("[{}] Saving relation: {}", id, relationDef); - EntityRelation entityRelation = new EntityRelation(); - EntityId otherId = resolveRelatedEntityId(relationDef, ctx); - if (EntitySearchDirection.FROM.equals(relationDef.getDirection())) { - entityRelation.setFrom(otherId); - entityRelation.setTo(id); - } else { - entityRelation.setFrom(id); - entityRelation.setTo(otherId); - } - entityRelation.setTypeGroup(RelationTypeGroup.COMMON); - entityRelation.setType(relationDef.getType()); - try { - relationService.save(ctx.getTenantId(), null, entityRelation, null); - } catch (Exception e) { - log.info("[{}] Failed to save relation: {}, cause: {}", id, relationDef, e.getMessage()); - } - } - }); - } - - private EntityId resolveRelatedEntityId(RelationDefinition relationDef, SolutionInstallContext ctx) { - if (relationDef.getEntityType() == EntityType.TENANT) { - return ctx.getTenantId(); - } - return ctx.getIdFromMap(relationDef.getEntityType(), relationDef.getEntityName()); - } - - private String prepareInstructions(SolutionInstallContext ctx, HttpServletRequest request) { - Path instructionsFile = ctx.getTempDir().resolve("instructions.md"); - if (!Files.exists(instructionsFile)) { - return null; - } - String template; - try { - template = Files.readString(instructionsFile); - } catch (IOException e) { - log.warn("[{}] Failed to read instructions.md", ctx.getTenantId(), e); - return null; - } - - String baseUrl = systemSecurityService.getBaseUrl(ctx.getTenantId(), null, request); - - // Inject edge instructions first, then run the full replacement logic on the combined string - if (template.contains("${edge_instructions}")) { - if (ctx.getCreatedEdges().isEmpty()) { - template = template.replace("${edge_instructions}", ""); - } else { - Path edgeFile = ctx.getTempDir().resolve("edge_instructions.md"); - String edgeTemplate = Files.exists(edgeFile) ? readFileContent(edgeFile) : ""; - template = template.replace("${edge_instructions}", edgeTemplate); - } - } - - template = template.replace("${DOCS_BASE_URL}", docsBaseUrl); - template = template.replace("${BASE_URL}", baseUrl); - - TenantSolutionTemplateInstructions solutionInstructions = ctx.getSolutionInstructions(); - - if (solutionInstructions.getDashboardId() != null) { - template = template.replace("${MAIN_DASHBOARD_URL}", - getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), false)); - if (solutionInstructions.isMainDashboardPublic()) { - template = template.replace("${MAIN_DASHBOARD_PUBLIC_URL}", - getDashboardLink(solutionInstructions, solutionInstructions.getDashboardId(), true)); - } - } - - for (DashboardLinkInfo dashboardLinkInfo : ctx.getDashboardLinks()) { - template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_URL}", - getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), false)); - if (dashboardLinkInfo.isPublic()) { - template = template.replace("${" + dashboardLinkInfo.getName() + "DASHBOARD_PUBLIC_URL}", - getDashboardLink(solutionInstructions, dashboardLinkInfo.getDashboardId(), true)); - } - } - - if (template.contains("${GATEWAYS_URL}")) { - template = template.replace("${GATEWAYS_URL}", "/gateways"); - } - - // Device list and credentials - StringBuilder devList = new StringBuilder(); - devList.append("| Device name | Access token | Owner |"); - devList.append(System.lineSeparator()); - devList.append("| :--- | :--- | :--- |"); - devList.append(System.lineSeparator()); - - for (DeviceCredentialsInfo credentialsInfo : ctx.getCreatedDevices().values()) { - devList.append("|").append(credentialsInfo.getName()) - .append("|").append(credentialsInfo.getCredentials().getCredentialsId()).append("{:copy-code}") - .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : "Tenant"); - devList.append(System.lineSeparator()); - - template = template.replace("${" + credentialsInfo.getName() + "ACCESS_TOKEN}", credentialsInfo.getCredentials().getCredentialsId()); - - if (credentialsInfo.isGateway()) { - template = template.replace("${DOCKER_CONFIG}", - prepareDockerComposeFile(ctx.getTenantId(), ctx.getSolutionId(), baseUrl, credentialsInfo.getCredentials().getDeviceId())); - } - } - - template = template.replace("${device_list_and_credentials}", devList.toString()); - - // User list (without user group column) - StringBuilder userList = new StringBuilder(); - userList.append("| Name | Login | Password | Customer name |"); - userList.append(System.lineSeparator()); - userList.append("| :--- | :--- | :--- | :--- |"); - userList.append(System.lineSeparator()); - - for (UserCredentialsInfo credentialsInfo : ctx.getCreatedUsers().values()) { - userList.append("|").append(credentialsInfo.getName()) - .append("|").append(credentialsInfo.getLogin()).append("{:copy-code}") - .append("|").append(credentialsInfo.getPassword()).append("{:copy-code}") - .append("|").append(credentialsInfo.getCustomerName() != null ? credentialsInfo.getCustomerName() : ""); - userList.append(System.lineSeparator()); - } - - template = template.replace("${user_list}", userList.toString()); - - // Edge detail URLs - for (Map.Entry edgeLinkInfoEntry : ctx.getCreatedEdges().entrySet()) { - EdgeLinkInfo edgeLinkInfo = edgeLinkInfoEntry.getValue(); - StringBuilder edgeDetailsUrl = new StringBuilder(); - if (EntityType.CUSTOMER.equals(edgeLinkInfo.getOwnerId().getEntityType())) { - edgeDetailsUrl.append("/customers/").append(edgeLinkInfo.getOwnerId().getId()); - edgeDetailsUrl.append("/edgeInstances/").append(edgeLinkInfo.getEdgeId().getId()); - } else { - edgeDetailsUrl.append("/edgeManagement/instances/").append(edgeLinkInfo.getEdgeId().getId()); - } - String edgeName = edgeLinkInfoEntry.getKey(); - String edgeDetailsPlaceholder = "${" + edgeName + "EDGE_DETAILS_URL}"; - template = template.replace(edgeDetailsPlaceholder, edgeDetailsUrl.toString()); - } - - template = replaceAlarmRules(ctx, template); - template = replaceCalculatedFields(ctx, template); - template = replaceCreatedEntities(ctx, template); - - return template; - } - - private static String replaceAlarmRules(SolutionInstallContext ctx, String template) { - StringBuilder alarmRules = new StringBuilder(); - - alarmRules.append("| Entity Profile Name | Alarm Type | Severities |").append(System.lineSeparator()); - alarmRules.append("| :--- | :--- | :--- |").append(System.lineSeparator()); - - ctx.getCreatedAlarmRules().entrySet().stream() - .sorted(Map.Entry.comparingByValue(Comparator.comparing(CreatedAlarmRuleInfo::entityName, String.CASE_INSENSITIVE_ORDER) - .thenComparing(CreatedAlarmRuleInfo::alarmType, String.CASE_INSENSITIVE_ORDER))) - .forEach(entry -> { - UUID key = entry.getKey(); - var alarmRuleInfo = entry.getValue(); - - String alarmType = alarmRuleInfo.alarmType(); - String link = alarmRuleInfo.getCfPageLink(key); - - String alarmTypeWithLink = "" + alarmType + ""; - - String profileName = alarmRuleInfo.entityId() != null ? - "" + alarmRuleInfo.entityName() + "" - : alarmRuleInfo.entityName(); - - alarmRules.append("|") - .append(profileName).append("|") - .append(alarmTypeWithLink).append("|") - .append(alarmRuleInfo.severities()).append("|") - .append(System.lineSeparator()); - }); - - return template.replace("${alarm_rules}", alarmRules.toString()); - } - - private static String replaceCalculatedFields(SolutionInstallContext ctx, String template) { - StringBuilder calculatedFields = new StringBuilder(); - - calculatedFields.append("| Entity Profile Name | Field Name | Field Type |").append(System.lineSeparator()); - calculatedFields.append("| :--- | :--- | :--- |").append(System.lineSeparator()); - - ctx.getCreatedCalculatedFields().entrySet().stream() - .sorted(Map.Entry.comparingByValue( - Comparator.comparing(CreatedCalculatedFieldInfo::entityName, String.CASE_INSENSITIVE_ORDER) - .thenComparing(CreatedCalculatedFieldInfo::name, String.CASE_INSENSITIVE_ORDER) - )) - .forEach(entry -> { - UUID key = entry.getKey(); - var cfInfo = entry.getValue(); - - String cfTitle = cfInfo.name(); - String link = cfInfo.getCfPageLink(key); - - String cfTitleWithLink = "" + cfTitle + ""; - - String profileName = cfInfo.entityId() != null ? - "" + cfInfo.entityName() + "" - : cfInfo.entityName(); - - calculatedFields.append("|") - .append(profileName).append("|") - .append(cfTitleWithLink).append("|") - .append(cfInfo.type()).append("|") - .append(System.lineSeparator()); - }); - - return template.replace("${calculated_fields}", calculatedFields.toString()); - } - - private static String replaceCreatedEntities(SolutionInstallContext ctx, String template) { - StringBuilder entityList = new StringBuilder(); - - entityList.append("| Name | Type | Owner |").append(System.lineSeparator()); - entityList.append("| :--- | :--- | :--- |").append(System.lineSeparator()); - - for (Map.Entry entry : ctx.getCreatedEntities().entrySet()) { - UUID key = entry.getKey(); - var entityInfo = entry.getValue(); - String link = entityInfo.getEntityPageLink(key); - String entityName = entityInfo.getName(); + private CalculatedField createCalculatedField(CalculatedField cf, SolutionInstallContext ctx) { + cf.setId(null); + cf.setCreatedTime(0L); + cf.setTenantId(ctx.getTenantId()); + cf.setDebugSettings(new DebugSettings(true, System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15))); - String name = link != null ? - "" + entityName + "" - : entityName; + Map realIds = ctx.getRealIds(); - entityList.append("|") - .append(name).append("|") - .append(entityInfo.getType().getNormalName()).append("|") - .append(entityInfo.getOwner()).append("|") - .append(System.lineSeparator()); + EntityId entityId = cf.getEntityId(); + if (entityId != null) { + String newEntityId = realIds.get(entityId.getId().toString()); + if (newEntityId != null) { + cf.setEntityId(EntityIdFactory.getByTypeAndUuid(entityId.getEntityType(), newEntityId)); + } else { + log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); + throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); + } } - return template.replace("${all_entities}", entityList.toString()); + if (cf.getConfiguration() instanceof BaseCalculatedFieldConfiguration baseCfg) { + baseCfg.getArguments().forEach((key, argument) -> { + EntityId refEntityId = argument.getRefEntityId(); + if (refEntityId != null) { + if (refEntityId.getEntityType() == EntityType.TENANT) { + argument.setRefEntityId(ctx.getTenantId()); + } else { + String newId = realIds.get(refEntityId.getId().toString()); + if (newId != null) { + argument.setRefEntityId(EntityIdFactory.getByTypeAndUuid(refEntityId.getEntityType(), newId)); + } else { + log.error("[{}] Calculated field: {} references non existing entity.", ctx.getTenantId(), cf.getName()); + throw new RuntimeException("Calculated field: " + cf.getName() + " references non existing entity."); + } + } + } + }); + } + + CalculatedField calculatedField = new CalculatedField(cf); + return calculatedFieldService.save(calculatedField); } private RandomNameData generateRandomName(SolutionInstallContext ctx) { @@ -1322,41 +1432,117 @@ public class DefaultSolutionService implements SolutionService { return String.valueOf(now.toInstant().toEpochMilli()); } - private String getDashboardLink(TenantSolutionTemplateInstructions solutionInstructions, DashboardId dashboardId, boolean isPublic) { - if (isPublic && solutionInstructions.getPublicId() != null) { - return "/dashboard/" + dashboardId.getId() + "?publicId=" + solutionInstructions.getPublicId(); + private String getTypeLabel(EntityType type) { + return type.name().toLowerCase().replace('_', ' '); + } + + private T loadEntityIfFileExists(Path tempDir, String fileName, Class clazz) { + Path filePath = tempDir.resolve("entities").resolve(fileName); + if (Files.exists(filePath)) { + return JacksonUtil.readValue(filePath.toFile(), clazz); + } else { + return null; } - return "/dashboards/" + dashboardId.getId(); } - private String prepareDockerComposeFile(TenantId tenantId, String solutionId, String baseUrl, DeviceId deviceId) { - Device device = new Device(deviceId); - device.setTenantId(tenantId); - String containerName = "tb-gateway-" + solutionId.replace('_', '-'); - DockerComposeParams params = new DockerComposeParams(false, containerName, false, true, false, false); - try (InputStream inputStream = deviceConnectivityService.createGatewayDockerComposeFile(baseUrl, device, params).getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) - ) { - return reader.lines().collect(Collectors.joining("\n")); - } catch (Exception e) { - throw new RuntimeException("Failed to read or process the docker-compose.yml file.", e); + private List loadListOfEntitiesIfFileExists(Path tempDir, String fileName, TypeReference> typeReference) { + Path filePath = tempDir.resolve("entities").resolve(fileName); + if (Files.exists(filePath)) { + try { + return JacksonUtil.readValue(filePath.toFile(), typeReference); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid json file " + fileName + " data structure", e); + } + } else { + return new ArrayList<>(); } } - private void rollback(TenantId tenantId, SolutionInstallContext ctx, Throwable e) { - List createdEntities = ctx.getCreatedEntitiesList(); - Collections.reverse(createdEntities); - for (EntityId entityId : createdEntities) { + private List loadListOfEntitiesFromDirectory(Path tempDir, String dirName, Class clazz) { + Path dirPath = tempDir.resolve(dirName); + if (Files.exists(dirPath) && Files.isDirectory(dirPath)) { + List result = new ArrayList<>(); try { - deleteEntity(tenantId, entityId, ctx.getUser()); - } catch (RuntimeException re) { - log.error("[{}] Failed to delete the entity: {}", tenantId, entityId, re); + for (Path filePath : Files.list(dirPath).collect(Collectors.toList())) { + try { + result.add(JacksonUtil.readValue(filePath.toFile(), clazz)); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid json file " + filePath.getFileName() + " data structure", e); + } + } + } catch (IOException e) { + log.warn("Failed to read directory: {}", dirName, e); + throw new RuntimeException(e); } + return result; + } else { + return new ArrayList<>(); + } + } + + private String loadSolutionId(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("title")) { + String title = node.get("title").asText(""); + return title.trim().toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", ""); + } + } + return null; + } + + private long loadInstallTimeoutMs(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("installTimeoutMs")) { + return node.get("installTimeoutMs").asLong(0L); + } + } + return 0L; + } + + private static void extractZip(byte[] zipData, Path destDir) throws IOException { + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + Path entryPath = destDir.resolve(entry.getName()).normalize(); + if (!entryPath.startsWith(destDir)) { + throw new IOException("ZIP entry outside of target directory: " + entry.getName()); + } + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + Files.createDirectories(entryPath.getParent()); + Files.write(entryPath, zis.readAllBytes()); + } + zis.closeEntry(); + } + } + } + + private static void deleteDirectory(Path dir) { + try { + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path d, IOException exc) throws IOException { + Files.delete(d); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + log.warn("Failed to clean up temp directory: {}", dir, e); } - ctx.getSolutionInstructions().setDetails(e.getMessage()); } - private void deleteEntity(TenantId tenantId, EntityId entityId, SecurityUser user) { + private void deleteEntity(TenantId tenantId, EntityId entityId, User user) { try { List alarmIds = alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)) .getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()); @@ -1439,87 +1625,4 @@ public class DefaultSolutionService implements SolutionService { } } - private T loadEntityIfFileExists(Path tempDir, String fileName, Class clazz) { - Path filePath = tempDir.resolve("entities").resolve(fileName); - if (Files.exists(filePath)) { - return JacksonUtil.readValue(filePath.toFile(), clazz); - } else { - return null; - } - } - - private List loadListOfEntitiesIfFileExists(Path tempDir, String fileName, TypeReference> typeReference) { - Path filePath = tempDir.resolve("entities").resolve(fileName); - if (Files.exists(filePath)) { - try { - return JacksonUtil.readValue(filePath.toFile(), typeReference); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid json file " + fileName + " data structure", e); - } - } else { - return new ArrayList<>(); - } - } - - private List loadListOfEntitiesFromDirectory(Path tempDir, String dirName, Class clazz) { - Path dirPath = tempDir.resolve(dirName); - if (Files.exists(dirPath) && Files.isDirectory(dirPath)) { - List result = new ArrayList<>(); - try { - for (Path filePath : Files.list(dirPath).collect(Collectors.toList())) { - try { - result.add(JacksonUtil.readValue(filePath.toFile(), clazz)); - } catch (Exception e) { - throw new IllegalArgumentException("Invalid json file " + filePath.getFileName() + " data structure", e); - } - } - } catch (IOException e) { - log.warn("Failed to read directory: {}", dirName, e); - throw new RuntimeException(e); - } - return result; - } else { - return new ArrayList<>(); - } - } - - private static void extractZip(byte[] zipData, Path destDir) throws IOException { - try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - Path entryPath = destDir.resolve(entry.getName()).normalize(); - if (!entryPath.startsWith(destDir)) { - throw new IOException("ZIP entry outside of target directory: " + entry.getName()); - } - if (entry.isDirectory()) { - Files.createDirectories(entryPath); - } else { - Files.createDirectories(entryPath.getParent()); - Files.write(entryPath, zis.readAllBytes()); - } - zis.closeEntry(); - } - } - } - - private static void deleteDirectory(Path dir) { - try { - Files.walkFileTree(dir, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path d, IOException exc) throws IOException { - Files.delete(d); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - log.warn("Failed to clean up temp directory: {}", dir, e); - } - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java index e852f569a9..9ccf136678 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.service.solutions.data.definition.AssetDefinition; import org.thingsboard.server.service.solutions.data.definition.AssetProfileDefinition; @@ -57,7 +56,7 @@ public class SolutionInstallContext { private final TenantId tenantId; private final Path tempDir; - private final SecurityUser user; + private final User user; private final TenantSolutionTemplateInstructions solutionInstructions; private final List createdEntitiesList = new ArrayList<>(); private final Map realIds = new HashMap<>(); @@ -78,12 +77,12 @@ public class SolutionInstallContext { private Map deviceEmulators; private Map assetEmulators; - public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, SecurityUser user) { + public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, User user, TenantSolutionTemplateInstructions solutionInstructions) { this.tenantId = tenantId; this.solutionId = solutionId; this.tempDir = tempDir; this.user = user; - this.solutionInstructions = new TenantSolutionTemplateInstructions(); + this.solutionInstructions = solutionInstructions; this.installTs = System.currentTimeMillis(); put(new EntitySearchKey(tenantId, EntityType.TENANT, null), tenantId); }