diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java index e055c6d6ae..72c339c635 100644 --- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java @@ -477,7 +477,7 @@ public class NotificationController extends BaseController { SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @GetMapping("/notification/deliveryMethods") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - public Set getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { + public List getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java index 00797da449..da2b7276dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/ResourcesUpdater.java @@ -101,7 +101,7 @@ public class ResourcesUpdater { for (DashboardId dashboardId : dashboards) { executor.submit(() -> { Dashboard dashboard = dashboardService.findDashboardById(TenantId.SYS_TENANT_ID, dashboardId); - boolean updated = resourceService.updateResourcesUsage(dashboard); // will convert resources ids to new structure + boolean updated = resourceService.updateResourcesUsage(dashboard.getTenantId(), dashboard); // will convert resources ids to new structure if (updated) { dashboardService.saveDashboard(dashboard); updatedCount.incrementAndGet(); @@ -130,7 +130,7 @@ public class ResourcesUpdater { for (WidgetTypeId widgetTypeId : widgets) { executor.submit(() -> { WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId); - boolean updated = resourceService.updateResourcesUsage(widgetTypeDetails); + boolean updated = resourceService.updateResourcesUsage(widgetTypeDetails.getTenantId(), widgetTypeDetails); if (updated) { widgetTypeService.saveWidgetType(widgetTypeDetails); updatedCount.incrementAndGet(); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index 311fe6cb2c..af63256f0f 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -417,7 +417,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } @Override - public Set getAvailableDeliveryMethods(TenantId tenantId) { + public List getAvailableDeliveryMethods(TenantId tenantId) { return channels.values().stream() .filter(channel -> { try { @@ -428,7 +428,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple } }) .map(NotificationChannel::getDeliveryMethod) - .collect(Collectors.toSet()); + .sorted().toList(); } @Override 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 90ad3e38dd..e3a79a8ba5 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 @@ -46,7 +46,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; -import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -132,12 +132,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements @Override public List exportResources(Dashboard dashboard, SecurityUser user) throws ThingsboardException { - return exportResources(dashboard, imageService::getUsedImages, resourceService::getUsedResources, user); + return exportResources(() -> imageService.getUsedImages(dashboard), () -> resourceService.getUsedResources(user.getTenantId(), dashboard), user); } @Override public List exportResources(WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException { - return exportResources(widgetTypeDetails, imageService::getUsedImages, resourceService::getUsedResources, user); + return exportResources(() -> imageService.getUsedImages(widgetTypeDetails), () -> resourceService.getUsedResources(user.getTenantId(), widgetTypeDetails), user); } @Override @@ -153,13 +153,12 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements } } - private List exportResources(T entity, - Function> imagesProcessor, - Function> resourcesProcessor, + private List exportResources(Supplier> imagesProcessor, + Supplier> resourcesProcessor, SecurityUser user) throws ThingsboardException { List resources = new ArrayList<>(); - resources.addAll(imagesProcessor.apply(entity)); - resources.addAll(resourcesProcessor.apply(entity)); + resources.addAll(imagesProcessor.get()); + resources.addAll(resourcesProcessor.get()); for (TbResourceInfo resourceInfo : resources) { accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 88dd8f241c..ffbf95d990 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -602,13 +603,14 @@ public class DashboardControllerTest extends AbstractControllerTest { dashboard.setTitle("My dashboard"); dashboard.setConfiguration(JacksonUtil.newObjectNode() .put("someImage", "tb-image;/api/images/tenant/" + imageInfo.getResourceKey()) - .set("widgets", JacksonUtil.toJsonNode(""" + .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", "tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js")); dashboard = doPost("/api/dashboard", dashboard, Dashboard.class); Dashboard exportedDashboard = doGet("/api/dashboard/" + dashboard.getUuidId() + "?includeResources=true", Dashboard.class); @@ -637,12 +639,18 @@ public class DashboardControllerTest extends AbstractControllerTest { doPost("/api/resource", resource, TbResourceInfo.class); Dashboard importedDashboard = doPost("/api/dashboard", exportedDashboard, Dashboard.class); + String newResourceKey = "gateway-management-extension_(1).js"; + imageRef = importedDashboard.getConfiguration().get("someImage").asText(); assertThat(imageRef).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey()); - resourceRef = importedDashboard.getConfiguration().get("widgets").get("xxx").get("config") - .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText(); - String newResourceKey = "gateway-management-extension_(1).js"; - assertThat(resourceRef).isEqualTo("tb-resource;/api/resource/js_module/tenant/" + newResourceKey); + + List resourcesRefs = new ArrayList<>(); + resourcesRefs.add(importedDashboard.getConfiguration().get("widgets").get("xxx").get("config") + .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText()); + resourcesRefs.add(importedDashboard.getConfiguration().get("someResource").asText()); + assertThat(resourcesRefs).allSatisfy(ref -> { + assertThat(ref).isEqualTo("tb-resource;/api/resource/js_module/tenant/" + newResourceKey); + }); TbResourceInfo importedImageInfo = doGet("/api/images/tenant/" + imageInfo.getResourceKey() + "/info", TbResourceInfo.class); assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag()); 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 0e2acb2e4e..f7f4b1ce4b 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 @@ -80,13 +80,13 @@ public interface ResourceService extends EntityDaoService { TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag); - boolean updateResourcesUsage(Dashboard dashboard); + boolean updateResourcesUsage(TenantId tenantId, Dashboard dashboard); - boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails); + boolean updateResourcesUsage(TenantId tenantId, WidgetTypeDetails widgetTypeDetails); - Collection getUsedResources(Dashboard dashboard); + Collection getUsedResources(TenantId tenantId, Dashboard dashboard); - Collection getUsedResources(WidgetTypeDetails widgetTypeDetails); + Collection getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails); TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, byte[] data); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java index d025aab3d5..fed271cbbe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java @@ -50,7 +50,8 @@ public class WidgetType extends BaseWidgetType { @JsonIgnore public JsonNode getDefaultConfig() { - return Optional.ofNullable(descriptor.get("defaultConfig")) + return Optional.ofNullable(descriptor) + .map(descriptor -> descriptor.get("defaultConfig")) .filter(JsonNode::isTextual).map(JsonNode::asText) .map(json -> { try { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index b547de1eca..2e9e37577c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -162,18 +162,19 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); } try { + TenantId tenantId = dashboard.getTenantId(); if (CollectionUtils.isNotEmpty(dashboard.getResources())) { - resourceService.importResources(dashboard.getTenantId(), dashboard.getResources()); + resourceService.importResources(tenantId, dashboard.getResources()); } imageService.updateImagesUsage(dashboard); - resourceService.updateResourcesUsage(dashboard); + resourceService.updateResourcesUsage(tenantId, dashboard); - var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); + var saved = dashboardDao.save(tenantId, dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId) .entityId(saved.getId()).created(dashboard.getId() == null).build()); if (dashboard.getId() == null) { - countService.publishCountEntityEvictEvent(saved.getTenantId(), EntityType.DASHBOARD); + countService.publishCountEntityEvictEvent(tenantId, EntityType.DASHBOARD); } return saved; } catch (Exception e) { 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 22aa2ae748..9395aae6c7 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 @@ -57,6 +57,7 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; @@ -401,18 +402,33 @@ public class BaseResourceService extends AbstractCachedEntityService links = getResourcesLinks(dashboard.getResources()); - return updateResourcesUsage(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING, links); + return updateResourcesUsage(tenantId, List.of(dashboard.getConfiguration()), List.of(DASHBOARD_RESOURCES_MAPPING), links); } @Override - public boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails) { + public boolean updateResourcesUsage(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { Map links = getResourcesLinks(widgetTypeDetails.getResources()); - boolean updated = updateResourcesUsage(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING, links); + List jsonNodes = new ArrayList<>(2); + List> mappings = new ArrayList<>(2); + + if (widgetTypeDetails.getDescriptor() != null) { + jsonNodes.add(widgetTypeDetails.getDescriptor()); + mappings.add(WIDGET_RESOURCES_MAPPING); + } + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { - updated |= updateResourcesUsage(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING, links); + jsonNodes.add(defaultConfig); + mappings.add(WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING); + } + + boolean updated = updateResourcesUsage(tenantId, jsonNodes, mappings, links); + if (defaultConfig != null) { widgetTypeDetails.setDefaultConfig(defaultConfig); } return updated; @@ -433,8 +449,9 @@ public class BaseResourceService extends AbstractCachedEntityService mapping, Map links) { - return processResources(jsonNode, mapping, value -> { + private boolean updateResourcesUsage(TenantId tenantId, List jsonNodes, List> mappings, Map links) { + log.trace("[{}] updateResourcesUsage (new links: {}) for {}", tenantId, links, jsonNodes); + return processResources(jsonNodes, mappings, value -> { String link = getResourceLink(value); if (link != null) { String newLink = links.get(link); @@ -462,23 +479,31 @@ public class BaseResourceService extends AbstractCachedEntityService getUsedResources(Dashboard dashboard) { - return getUsedResources(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING).values(); + public Collection getUsedResources(TenantId tenantId, Dashboard dashboard) { + return getUsedResources(tenantId, List.of(dashboard.getConfiguration()), List.of(DASHBOARD_RESOURCES_MAPPING)).values(); } @Override - public Collection getUsedResources(WidgetTypeDetails widgetTypeDetails) { - Map resources = getUsedResources(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); + public Collection getUsedResources(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) { + List jsonNodes = new ArrayList<>(2); + List> mappings = new ArrayList<>(2); + + jsonNodes.add(widgetTypeDetails.getDescriptor()); + mappings.add(WIDGET_RESOURCES_MAPPING); + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); if (defaultConfig != null) { - resources.putAll(getUsedResources(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING)); + jsonNodes.add(defaultConfig); + mappings.add(WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING); } - return resources.values(); + + return getUsedResources(tenantId, jsonNodes, mappings).values(); } - private Map getUsedResources(TenantId tenantId, JsonNode jsonNode, Map mapping) { + private Map getUsedResources(TenantId tenantId, List jsonNodes, List> mappings) { Map resources = new HashMap<>(); - processResources(jsonNode, mapping, value -> { + log.trace("[{}] getUsedResources for {}", tenantId, jsonNodes); + processResources(jsonNodes, mappings, value -> { String link = getResourceLink(value); if (link == null) { return value; @@ -517,32 +542,57 @@ public class BaseResourceService extends AbstractCachedEntityService mapping, UnaryOperator processor) { + private boolean processResources(List jsonNodes, List> mappings, UnaryOperator processor) { AtomicBoolean updated = new AtomicBoolean(false); - JacksonUtil.replaceByMapping(jsonNode, mapping, Collections.emptyMap(), (name, urlNode) -> { - String value = null; - if (urlNode.isTextual()) { // link is in the right place - value = urlNode.asText(); - } else { - JsonNode id = urlNode.get("id"); // old structure is used - if (id != null && id.isTextual()) { - value = id.asText(); + + for (int i = 0; i < jsonNodes.size(); i++) { + JsonNode jsonNode = jsonNodes.get(i); + // processing by mappings first + if (i <= mappings.size() - 1) { + JacksonUtil.replaceByMapping(jsonNode, mappings.get(i), Collections.emptyMap(), (name, urlNode) -> { + String value = null; + if (urlNode.isTextual()) { // link is in the right place + value = urlNode.asText(); + } else { + JsonNode id = urlNode.get("id"); // old structure is used + if (id != null && id.isTextual()) { + value = id.asText(); + } + } + + if (StringUtils.isNotBlank(value)) { + value = processor.apply(value); + } else { + value = ""; + } + + JsonNode newValue = new TextNode(value); + if (!newValue.toString().equals(urlNode.toString())) { + updated.set(true); + log.trace("Replaced by mapping '{}' ({}) with '{}'", value, name, newValue); + } + return newValue; + }); + } + + + // processing all + JacksonUtil.replaceAll(jsonNode, "", (name, value) -> { + if (!StringUtils.startsWith(value, DataConstants.TB_RESOURCE_PREFIX + "/api/resource/")) { + return value; } - } - if (StringUtils.isNotBlank(value)) { - value = processor.apply(value); - } else { - value = ""; - } + String newValue = processor.apply(value); + if (StringUtils.equals(value, newValue)) { + return value; + } else { + updated.set(true); + log.trace("Replaced '{}' ({}) with '{}'", value, name, newValue); + return newValue; + } + }); + } - JsonNode newValue = new TextNode(value); - if (!newValue.toString().equals(urlNode.toString())) { - updated.set(true); - log.trace("Replaced '{}' with '{}'", urlNode, newValue); - } - return newValue; - }); return updated.get(); } @@ -555,7 +605,7 @@ public class BaseResourceService extends AbstractCachedEntityService3.9.2 3.25.5 1.63.0 - 1.2.4 + 1.2.5 1.18.32 1.2.5 1.2.5 diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java index 02ee53f3ad..b8fb9b61ff 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; -import java.util.Set; +import java.util.List; public interface NotificationCenter { @@ -48,6 +48,6 @@ public interface NotificationCenter { void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId); - Set getAvailableDeliveryMethods(TenantId tenantId); + List getAvailableDeliveryMethods(TenantId tenantId); }