From 52a6499d1c0e64f2b76c63cbd8a71cef1a4b7f53 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 18 Oct 2024 12:50:03 +0300 Subject: [PATCH] Export/import used resources along with dashboard and widget type --- .../controller/ControllerConstants.java | 1 - .../controller/DashboardController.java | 15 +- .../controller/WidgetTypeController.java | 44 +++--- .../dashboard/DefaultTbDashboardService.java | 41 +++++- .../type/DefaultWidgetTypeService.java | 82 +++++++++-- .../widgets/type/TbWidgetTypeService.java | 8 ++ .../resource/DefaultTbResourceService.java | 55 +++++++ .../service/resource/TbResourceService.java | 8 ++ .../server/dao/resource/ResourceService.java | 15 ++ .../common/data/ResourceExportData.java | 4 + .../common/data/widget/WidgetsExportData.java | 16 +-- .../thingsboard/common/util/JacksonUtil.java | 118 +++++++++++++++ .../server/dao/resource/BaseImageService.java | 114 ++------------- .../dao/resource/BaseResourceService.java | 136 ++++++++++++++++-- .../dao/resource/TbResourceInfoDao.java | 2 +- .../sql/resource/JpaTbResourceInfoDao.java | 4 +- .../resource/TbResourceInfoRepository.java | 6 +- .../dao/util/JsonPathProcessingTask.java | 69 --------- 18 files changed, 487 insertions(+), 251 deletions(-) rename dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java => common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java (67%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 4d83d4debb..a774e9f889 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -34,7 +34,6 @@ public class ControllerConstants { "See the 'Model' tab of the Response Class for more details. "; protected static final String INLINE_IMAGES = "inlineImages"; - protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index a602b257f9..e9efd4d226 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -78,8 +78,6 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; -import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -150,18 +148,11 @@ public class DashboardController extends BaseController { ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/dashboard/{dashboardId}") - public Dashboard getDashboardById( - @Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) - @PathVariable(DASHBOARD_ID) String strDashboardId, - @Parameter(description = INLINE_IMAGES_DESCRIPTION) - @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { + public Dashboard getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - var result = checkDashboardId(dashboardId, Operation.READ); - if (inlineImages) { - imageService.inlineImages(result); - } - return result; + return checkDashboardId(dashboardId, Operation.READ); } @GetMapping(value = "/dashboard/{dashboardId}/export") diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 48b6946d0c..70f56583e7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -47,9 +47,9 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeFilter; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.data.widget.WidgetsExportData; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService; import org.thingsboard.server.service.security.permission.Operation; @@ -61,8 +61,6 @@ import java.util.List; import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; -import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES; -import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -80,7 +78,6 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ public class WidgetTypeController extends AutoCommitController { private final TbWidgetTypeService tbWidgetTypeService; - private final ImageService imageService; private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory."; private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " + @@ -97,18 +94,11 @@ public class WidgetTypeController extends AutoCommitController { notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @GetMapping(value = "/widgetType/{widgetTypeId}") - public WidgetTypeDetails getWidgetTypeById( - @Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetTypeId") String strWidgetTypeId, - @Parameter(description = INLINE_IMAGES_DESCRIPTION) - @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { + public WidgetTypeDetails getWidgetTypeById(@Parameter(description = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); - var result = checkWidgetTypeId(widgetTypeId, Operation.READ); - if (inlineImages) { - imageService.inlineImages(result); - } - return result; + return checkWidgetTypeId(widgetTypeId, Operation.READ); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @@ -293,16 +283,26 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public List getBundleWidgetTypesDetails( @Parameter(description = "Widget Bundle Id", required = true) - @RequestParam("widgetsBundleId") String strWidgetsBundleId, - @Parameter(description = INLINE_IMAGES_DESCRIPTION) - @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages + @RequestParam("widgetsBundleId") String strWidgetsBundleId ) throws ThingsboardException { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - var result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); - if (inlineImages) { - result.forEach(imageService::inlineImages); - } - return result; + return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/widgetTypes/export", params = {"widgetsBundleId"}) + public WidgetsExportData exportBundleWidgetTypes(@Parameter(description = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); + return tbWidgetTypeService.exportBundleWidgetTypes(getTenantId(), widgetsBundleId, getCurrentUser()); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @PostMapping(value = "/widgetTypes/import", params = {"widgetsBundleId"}) + public List importBundleWidgetTypes(@Parameter(description = "Widget Bundle Id", required = true) + @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + // FIXME: determine how bundle widget types were imported before + throw new UnsupportedOperationException(); } @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java index 471b9cb45e..1cfabed966 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java @@ -32,18 +32,22 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.resource.TbImageService; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.HashMap; import java.util.HashSet; -import java.util.List; +import java.util.Map; import java.util.Set; @Service @@ -54,6 +58,8 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement private final DashboardService dashboardService; private final ImageService imageService; private final TbImageService tbImageService; + private final ResourceService resourceService; + private final TbResourceService tbResourceService; @Override public Dashboard save(Dashboard dashboard, User user) throws Exception { @@ -297,27 +303,48 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement @Override public DashboardExportData exportDashboard(TenantId tenantId, Dashboard dashboard, SecurityUser user) throws ThingsboardException { - List images = imageService.inlineImages(dashboard); - for (TbResourceInfo imageInfo : images) { - accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, imageInfo.getId(), imageInfo); + Map resources = new HashMap<>(); + for (TbResourceInfo imageInfo : imageService.inlineImages(dashboard)) { + resources.putIfAbsent(imageInfo.getId(), imageInfo); + } + for (TbResourceInfo resourceInfo : resourceService.processResourcesForExport(dashboard)) { + resources.putIfAbsent(resourceInfo.getId(), resourceInfo); + } + for (TbResourceInfo resourceInfo : resources.values()) { + accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo); } DashboardExportData exportData = new DashboardExportData(); exportData.setDashboard(dashboard); - exportData.setResources(images.stream() - .map(tbImageService::exportImage) + exportData.setResources(resources.values().stream() + .map(resourceInfo -> { + if (resourceInfo.getResourceType() == ResourceType.IMAGE) { + ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo); + imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import + return imageExportData; + } else { + return tbResourceService.exportResource(resourceInfo); + } + }) .toList()); return exportData; } @Override public Dashboard importDashboard(DashboardExportData exportData, SecurityUser user) throws Exception { + Map importedResources = new HashMap<>(); for (ResourceExportData resourceExportData : exportData.getResources()) { if (resourceExportData.getType() == ResourceType.IMAGE) { tbImageService.importImage(resourceExportData, true, user); + } else { + TbResourceInfo importedResource = tbResourceService.importResource(resourceExportData, true, user); + importedResources.put(resourceExportData.getId(), importedResource.getId()); } } - return save(exportData.getDashboard(), user); + + Dashboard dashboard = exportData.getDashboard(); + resourceService.processResourcesForImport(dashboard, importedResources); + return save(dashboard, user); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java index 6fe14a53dc..9b732a4872 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java @@ -25,20 +25,28 @@ import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetExportData; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsExportData; import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.resource.TbImageService; +import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service @TbCoreComponent @@ -49,6 +57,8 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements private final WidgetTypeService widgetTypeService; private final ImageService imageService; private final TbImageService tbImageService; + private final ResourceService resourceService; + private final TbResourceService tbResourceService; @Override public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception { @@ -92,27 +102,81 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements @Override public WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException { - List images = imageService.inlineImages(widgetTypeDetails); - for (TbResourceInfo imageInfo : images) { - accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, imageInfo.getId(), imageInfo); - } + List resources = processWidgetTypesForExport(List.of(widgetTypeDetails), user); WidgetExportData exportData = new WidgetExportData(); exportData.setWidgetTypeDetails(widgetTypeDetails); - exportData.setResources(images.stream() - .map(tbImageService::exportImage) - .toList()); + exportData.setResources(resources); + return exportData; + } + + @Override + public WidgetsExportData exportBundleWidgetTypes(TenantId tenantId, WidgetsBundleId bundleId, SecurityUser user) throws ThingsboardException { + List widgetTypes = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(tenantId, bundleId); + List resources = processWidgetTypesForExport(widgetTypes, user); + + WidgetsExportData exportData = new WidgetsExportData(); + exportData.setWidgetTypesDetails(widgetTypes); + exportData.setResources(resources); return exportData; } + private List processWidgetTypesForExport(List widgetTypes, SecurityUser user) throws ThingsboardException { + Map resources = new HashMap<>(); + for (WidgetTypeDetails widgetTypeDetails : widgetTypes) { + for (TbResourceInfo imageInfo : imageService.inlineImages(widgetTypeDetails)) { + resources.putIfAbsent(imageInfo.getId(), imageInfo); + } + for (TbResourceInfo resourceInfo : resourceService.processResourcesForExport(widgetTypeDetails)) { + resources.putIfAbsent(resourceInfo.getId(), resourceInfo); + } + } + for (TbResourceInfo resourceInfo : resources.values()) { + accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo); + } + + return resources.values().stream() + .map(resourceInfo -> { + if (resourceInfo.getResourceType() == ResourceType.IMAGE) { + ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo); + imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import + return imageExportData; + } else { + return tbResourceService.exportResource(resourceInfo); + } + }) + .toList(); + } + @Override public WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception { - for (ResourceExportData resourceExportData : exportData.getResources()) { + return importWidgetTypes(List.of(exportData.getWidgetTypeDetails()), exportData.getResources(), user).get(0); + } + + @Override + public List importWidgetTypes(WidgetsExportData exportData, SecurityUser user) throws Exception { + return importWidgetTypes(exportData.getWidgetTypesDetails(), exportData.getResources(), user); + } + + private List importWidgetTypes(List widgetTypesDetails, List resources, SecurityUser user) throws Exception { + Map importedResources = new HashMap<>(); + for (ResourceExportData resourceExportData : resources) { if (resourceExportData.getType() == ResourceType.IMAGE) { tbImageService.importImage(resourceExportData, true, user); + } else { + TbResourceInfo importedResource = tbResourceService.importResource(resourceExportData, true, user); + importedResources.put(resourceExportData.getId(), importedResource.getId()); } } - return save(exportData.getWidgetTypeDetails(), user); + + List result = new ArrayList<>(); + for (WidgetTypeDetails widgetTypeDetails : widgetTypesDetails) { + resourceService.processResourcesForImport(widgetTypeDetails, importedResources); + // FIXME: shouldn't just save it, have to be processed somehow + widgetTypeDetails = save(widgetTypeDetails, user); + result.add(widgetTypeDetails); + } + return result; } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java index 1b200bcb2e..e5a39910d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java @@ -18,17 +18,25 @@ package org.thingsboard.server.service.entitiy.widgets.type; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetExportData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsExportData; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.List; + public interface TbWidgetTypeService extends SimpleTbEntityService { WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception; WidgetExportData exportWidgetType(TenantId tenantId, WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException; + WidgetsExportData exportBundleWidgetTypes(TenantId tenantId, WidgetsBundleId bundleId, SecurityUser user) throws ThingsboardException; + WidgetTypeDetails importWidgetType(WidgetExportData exportData, SecurityUser user) throws Exception; + List importWidgetTypes(WidgetsExportData exportData, SecurityUser user) throws Exception; + } 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 b42f57116c..32a51db57c 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 @@ -19,8 +19,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ResourceExportData; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -32,12 +35,17 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; +import java.util.Base64; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwM2mObject; @@ -115,6 +123,53 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements .collect(Collectors.toList()); } + @Override + public ResourceExportData exportResource(TbResourceInfo resourceInfo) { + byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId()); + return ResourceExportData.builder() + .id(resourceInfo.getId()) + .mediaType(resourceInfo.getResourceType().getMediaType()) + .fileName(resourceInfo.getFileName()) + .title(resourceInfo.getTitle()) + .type(resourceInfo.getResourceType()) + .subType(resourceInfo.getResourceSubType()) + .resourceKey(resourceInfo.getResourceKey()) + .data(Base64.getEncoder().encodeToString(data)) + .build(); + } + + @Override + public TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException { + if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE + || exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) { + throw new IllegalArgumentException("Image import not supported"); + } + TbResource resource = new TbResource(); + resource.setTenantId(user.getTenantId()); + accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, resource); + + byte[] data = Base64.getDecoder().decode(exportData.getData()); + if (checkExisting) { + String etag = resourceService.calculateEtag(data); + TbResourceInfo existingResource = resourceService.findSystemOrTenantResourceByEtag(user.getTenantId(), exportData.getType(), etag); + if (existingResource != null) { + return existingResource; + } + } + + resource.setFileName(exportData.getFileName()); + if (isNotEmpty(exportData.getTitle())) { + resource.setTitle(exportData.getTitle()); + } else { + resource.setTitle(exportData.getFileName()); + } + resource.setResourceSubType(exportData.getSubType()); + resource.setResourceType(exportData.getType()); + resource.setResourceKey(exportData.getResourceKey()); + resource.setData(data); + return save(resource, user); + } + private Comparator getComparator(String sortProperty, String sortOrder) { Comparator comparator; if ("name".equals(sortProperty)) { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java index e05337d555..8cbb33db02 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.service.resource; +import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; import java.util.List; @@ -35,4 +39,8 @@ public interface TbResourceService extends SimpleTbEntityService { String sortOrder, PageLink pageLink); + ResourceExportData exportResource(TbResourceInfo resourceInfo); + + TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException; + } 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 18549ab0d6..37cf196410 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.resource; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -24,9 +25,11 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; +import java.util.Map; public interface ResourceService extends EntityDaoService { @@ -64,6 +67,18 @@ public interface ResourceService extends EntityDaoService { long sumDataSizeByTenantId(TenantId tenantId); + String calculateEtag(byte[] data); + + TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag); + + List processResourcesForExport(Dashboard dashboard); + + List processResourcesForExport(WidgetTypeDetails widgetTypeDetails); + + void processResourcesForImport(Dashboard dashboard, Map importedResources); + + void processResourcesForImport(WidgetTypeDetails widgetTypeDetails, Map importedResources); + TbResource updateSystemResource(ResourceType resourceType, String resourceKey, String data); String checkSystemResourcesUsage(String content, ResourceType... usedResourceTypes); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java index de531b55c3..d2c815cc2c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java @@ -15,12 +15,14 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.TbResourceId; @Schema @Slf4j @@ -28,8 +30,10 @@ import lombok.extern.slf4j.Slf4j; @NoArgsConstructor @AllArgsConstructor @Builder +@JsonInclude(JsonInclude.Include.NON_NULL) public class ResourceExportData { + private TbResourceId id; private String title; private ResourceType type; private ResourceSubType subType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java similarity index 67% rename from dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java rename to common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java index c4fd98b9e7..44a09e7cab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsExportData.java @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.util; +package org.thingsboard.server.common.data.widget; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; +import org.thingsboard.server.common.data.ResourceExportData; + +import java.util.List; @Data -public class JsonNodeProcessingTask { - private final String path; - private final JsonNode node; +public class WidgetsExportData { + private List widgetTypesDetails; + private List resources; - public JsonNodeProcessingTask(String path, JsonNode node) { - this.path = path; - this.node = node; - } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 77ded5d3ff..dd79bab265 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -467,6 +468,75 @@ public class JacksonUtil { } } + public static void replaceAllByMapping(JsonNode jsonNode, Map mapping, Map templateParams, BiFunction processor) { + for (var entry : mapping.entrySet()) { + String expression = entry.getValue(); + Queue tasks = new LinkedList<>(); + tasks.add(new JsonPathProcessingTask(entry.getKey().split("\\."), templateParams, jsonNode)); + while (!tasks.isEmpty()) { + JsonPathProcessingTask task = tasks.poll(); + String token = task.currentToken(); + JsonNode node = task.getNode(); + if (node == null) { + continue; + } + if (token.equals("*") || token.startsWith("$")) { + String variableName = token.startsWith("$") ? token.substring(1) : null; + if (node.isArray()) { + ArrayNode childArray = (ArrayNode) node; + for (JsonNode element : childArray) { + tasks.add(task.next(element)); + } + } else if (node.isObject()) { + ObjectNode on = (ObjectNode) node; + for (Iterator> it = on.fields(); it.hasNext(); ) { + var kv = it.next(); + if (variableName != null) { + tasks.add(task.next(kv.getValue(), variableName, kv.getKey())); + } else { + tasks.add(task.next(kv.getValue())); + } + } + } + } else { + String variableName = null; + String variableValue = null; + if (token.contains("[$")) { + variableName = StringUtils.substringBetween(token, "[$", "]"); + token = StringUtils.substringBefore(token, "[$"); + } + if (node.has(token)) { + JsonNode value = node.get(token); + if (variableName != null && value.has(variableName) && value.get(variableName).isTextual()) { + variableValue = value.get(variableName).asText(); + } + if (task.isLast()) { + String name = expression; + for (var replacement : task.getVariables().entrySet()) { + name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); + } + if (node.isObject() && value.isTextual()) { + ((ObjectNode) node).put(token, processor.apply(name, value.asText())); + } else if (value.isArray()) { + ArrayNode array = (ArrayNode) value; + for (int i = 0; i < array.size(); i++) { + String arrayElementName = name.replace("$index", Integer.toString(i)); + array.set(i, processor.apply(arrayElementName, array.get(i).asText())); + } + } + } else { + if (StringUtils.isNotEmpty(variableName)) { + tasks.add(task.next(value, variableName, variableValue)); + } else { + tasks.add(task.next(value)); + } + } + } + } + } + } + } + @Data public static class JsonNodeProcessingTask { private final String path; @@ -476,6 +546,54 @@ public class JacksonUtil { this.path = path; this.node = node; } + + } + + @Data + public static class JsonPathProcessingTask { + private final String[] tokens; + private final Map variables; + private final JsonNode node; + + public JsonPathProcessingTask(String[] tokens, Map variables, JsonNode node) { + this.tokens = tokens; + this.variables = variables; + this.node = node; + } + + public boolean isLast() { + return tokens.length == 1; + } + + public String currentToken() { + return tokens[0]; + } + + public JsonPathProcessingTask next(JsonNode next) { + return new JsonPathProcessingTask( + Arrays.copyOfRange(tokens, 1, tokens.length), + variables, + next); + } + + public JsonPathProcessingTask next(JsonNode next, String key, String value) { + Map variables = new HashMap<>(this.variables); + variables.put(key, value); + return new JsonPathProcessingTask( + Arrays.copyOfRange(tokens, 1, tokens.length), + variables, + next); + } + + @Override + public String toString() { + return "JsonPathProcessingTask{" + + "tokens=" + Arrays.toString(tokens) + + ", variables=" + variables + + ", node=" + node.toString().substring(0, 20) + + '}'; + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 6aa685c40b..88b4ed234a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -16,9 +16,7 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Strings; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.SneakyThrows; @@ -55,7 +53,6 @@ import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage; -import org.thingsboard.server.dao.util.JsonPathProcessingTask; import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; @@ -64,12 +61,9 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Queue; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -143,7 +137,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic @SneakyThrows public TbResourceInfo saveImage(TbResource image) { if (image.getId() == null) { - image.setResourceKey(getUniqueKey(image.getTenantId(), StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName()))); + image.setResourceKey(getUniqueKey(image.getTenantId(), ResourceType.IMAGE, StringUtils.defaultIfEmpty(image.getResourceKey(), image.getFileName()))); } if (image.getResourceSubType() == null) { image.setResourceSubType(ResourceSubType.IMAGE); @@ -185,27 +179,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic return Pair.of(descriptor, preview.getData()); } - private String getUniqueKey(TenantId tenantId, String filename) { - if (!resourceInfoDao.existsByTenantIdAndResourceTypeAndResourceKey(tenantId, ResourceType.IMAGE, filename)) { - return filename; - } - - String basename = StringUtils.substringBeforeLast(filename, "."); - String extension = StringUtils.substringAfterLast(filename, "."); - - Set existing = resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix( - tenantId, ResourceType.IMAGE, basename - ); - String resourceKey = filename; - int idx = 1; - while (existing.contains(resourceKey)) { - resourceKey = basename + "_(" + idx + ")." + extension; - idx++; - } - log.debug("[{}] Generated unique key {} for image {}", tenantId, resourceKey, filename); - return resourceKey; - } - private String generatePublicResourceKey() { return RandomStringUtils.randomAlphanumeric(32); } @@ -296,11 +269,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic @Override public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) { - if (StringUtils.isEmpty(etag)) { - return null; - } - log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag); - return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag); + return findSystemOrTenantResourceByEtag(tenantId, ResourceType.IMAGE, etag); } @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure @@ -358,78 +327,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic } private boolean base64ToImageUrlUsingMapping(TenantId tenantId, Map mapping, Map templateParams, JsonNode configuration) { - boolean updated = false; - for (var entry : mapping.entrySet()) { - String expression = entry.getValue(); - Queue tasks = new LinkedList<>(); - tasks.add(new JsonPathProcessingTask(entry.getKey().split("\\."), templateParams, configuration)); - while (!tasks.isEmpty()) { - JsonPathProcessingTask task = tasks.poll(); - String token = task.currentToken(); - JsonNode node = task.getNode(); - if (node == null) { - continue; - } - if (token.equals("*") || token.startsWith("$")) { - String variableName = token.startsWith("$") ? token.substring(1) : null; - if (node.isArray()) { - ArrayNode childArray = (ArrayNode) node; - for (JsonNode element : childArray) { - tasks.add(task.next(element)); - } - } else if (node.isObject()) { - ObjectNode on = (ObjectNode) node; - for (Iterator> it = on.fields(); it.hasNext(); ) { - var kv = it.next(); - if (variableName != null) { - tasks.add(task.next(kv.getValue(), variableName, kv.getKey())); - } else { - tasks.add(task.next(kv.getValue())); - } - } - } - } else { - String variableName = null; - String variableValue = null; - if (token.contains("[$")) { - variableName = StringUtils.substringBetween(token, "[$", "]"); - token = StringUtils.substringBefore(token, "[$"); - } - if (node.has(token)) { - JsonNode value = node.get(token); - if (variableName != null && value.has(variableName) && value.get(variableName).isTextual()) { - variableValue = value.get(variableName).asText(); - } - if (task.isLast()) { - String name = expression; - for (var replacement : task.getVariables().entrySet()) { - name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); - } - if (node.isObject() && value.isTextual()) { - var result = base64ToImageUrl(tenantId, name, value.asText()); - ((ObjectNode) node).put(token, result.getValue()); - updated |= result.isUpdated(); - } else if (value.isArray()) { - ArrayNode array = (ArrayNode) value; - for (int i = 0; i < array.size(); i++) { - String arrayElementName = name.replace("$index", Integer.toString(i)); - UpdateResult result = base64ToImageUrl(tenantId, arrayElementName, array.get(i).asText()); - array.set(i, result.getValue()); - updated |= result.isUpdated(); - } - } - } else { - if (StringUtils.isNotEmpty(variableName)) { - tasks.add(task.next(value, variableName, variableValue)); - } else { - tasks.add(task.next(value)); - } - } - } - } + AtomicBoolean updated = new AtomicBoolean(false); + JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> { + UpdateResult result = base64ToImageUrl(tenantId, name, value); + if (result.isUpdated()) { + updated.set(true); } - } - return updated; + return result.getValue(); + }); + return updated.get(); } private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data) { 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 bed84027f4..2c76f9bb36 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 @@ -15,17 +15,21 @@ */ package org.thingsboard.server.dao.resource; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.RegexUtils; import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; @@ -37,6 +41,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -46,8 +51,13 @@ 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.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -63,9 +73,20 @@ public class BaseResourceService extends AbstractCachedEntityService DASHBOARD_RESOURCES_MAPPING = Map.of( + "widgets.*.config.actions.*.*.customResources.*.url.id", "" + ); + + private static final Map WIDGET_RESOURCES_MAPPING = Map.of( + "descriptor.resources.*.url.id", "" + ); + @Override public TbResource saveResource(TbResource resource, boolean doValidate) { log.trace("Executing saveResource [{}]", resource); + if (resource.getId() == null) { + resource.setResourceKey(getUniqueKey(resource.getTenantId(), resource.getResourceType(), StringUtils.defaultIfEmpty(resource.getResourceKey(), resource.getFileName()))); + } if (doValidate) { resourceValidator.validate(resource, TbResourceInfo::getTenantId); } @@ -109,6 +130,27 @@ public class BaseResourceService extends AbstractCachedEntityService existing = resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix( + tenantId, resourceType, basename + ); + String resourceKey = filename; + int idx = 1; + while (existing.contains(resourceKey)) { + resourceKey = basename + "_(" + idx + ")." + extension; + idx++; + } + log.debug("[{}] Generated unique key {} for {} {}", tenantId, resourceKey, resourceType, filename); + return resourceKey; + } + @Override public TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) { log.trace("Executing findResourceByTenantIdAndKey [{}] [{}] [{}]", tenantId, resourceType, resourceKey); @@ -239,6 +281,67 @@ public class BaseResourceService extends AbstractCachedEntityService processResourcesForExport(Dashboard dashboard) { + return processResourcesForExport(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING); + } + + @Override + public List processResourcesForExport(WidgetTypeDetails widgetTypeDetails) { + return processResourcesForExport(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); + } + + private List processResourcesForExport(TenantId tenantId, JsonNode jsonNode, Map mapping) { + List resources = new ArrayList<>(); + JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { + if (StringUtils.isBlank(value)) { + return value; + } + + TbResourceId resourceId; + try { + resourceId = new TbResourceId(UUID.fromString(value)); + } catch (IllegalArgumentException e) { + return value; + } + TbResourceInfo resourceInfo = findResourceInfoById(tenantId, resourceId); + resources.add(resourceInfo); + return value; + }); + return resources; + } + + @Override + public void processResourcesForImport(Dashboard dashboard, Map importedResources) { + processResourcesForImport(dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING, importedResources); + } + + @Override + public void processResourcesForImport(WidgetTypeDetails widgetTypeDetails, Map importedResources) { + processResourcesForImport(widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING, importedResources); + } + + private void processResourcesForImport(JsonNode jsonNode, Map mapping, Map importedResources) { + JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { + if (StringUtils.isBlank(value)) { + return value; + } + + TbResourceId oldResourceId; + try { + oldResourceId = new TbResourceId(UUID.fromString(value)); + } catch (IllegalArgumentException e) { + return value; + } + TbResourceId importedResourceId = importedResources.get(oldResourceId); + if (importedResourceId != null) { + return importedResourceId.toString(); + } else { + return value; + } + }); + } + @Override public TbResource updateSystemResource(ResourceType resourceType, String resourceKey, String data) { if (resourceType == ResourceType.DASHBOARD) { @@ -274,23 +377,32 @@ public class BaseResourceService extends AbstractCachedEntityService tenantResourcesRemover = - new PaginatedRemover<>() { + @Override + public TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag) { + if (StringUtils.isEmpty(etag)) { + return null; + } + log.trace("Executing findSystemOrTenantResourceByEtag [{}] [{}] [{}]", tenantId, resourceType, etag); + return resourceInfoDao.findSystemOrTenantResourceByEtag(tenantId, resourceType, etag); + } + + private final PaginatedRemover tenantResourcesRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return resourceDao.findAllByTenantId(id, pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return resourceDao.findIdsByTenantId(id.getId(), pageLink); + } - @Override - protected void removeEntity(TenantId tenantId, TbResource entity) { - deleteResource(tenantId, entity.getId()); - } - }; + @Override + protected void removeEntity(TenantId tenantId, TbResourceId resourceId) { + deleteResource(tenantId, resourceId); + } + }; @TransactionalEventListener(classes = ResourceInfoEvictEvent.class) @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 08b405886a..3ecc4255b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -40,7 +40,7 @@ public interface TbResourceInfoDao extends Dao { List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); - TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag); + TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag); boolean existsByPublicResourceKey(ResourceType resourceType, String publicResourceKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java index 4d90df176d..1bf7ed0aa4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java @@ -113,8 +113,8 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao variables; - private final JsonNode node; - - public JsonPathProcessingTask(String[] tokens, Map variables, JsonNode node) { - this.tokens = tokens; - this.variables = variables; - this.node = node; - } - - public boolean isLast() { - return tokens.length == 1; - } - - public String currentToken() { - return tokens[0]; - } - - public JsonPathProcessingTask next(JsonNode next) { - return new JsonPathProcessingTask( - Arrays.copyOfRange(tokens, 1, tokens.length), - variables, - next); - } - - public JsonPathProcessingTask next(JsonNode next, String key, String value) { - Map variables = new HashMap<>(this.variables); - variables.put(key, value); - return new JsonPathProcessingTask( - Arrays.copyOfRange(tokens, 1, tokens.length), - variables, - next); - } - - @Override - public String toString() { - return "JsonPathProcessingTask{" + - "tokens=" + Arrays.toString(tokens) + - ", variables=" + variables + - ", node=" + node.toString().substring(0, 20) + - '}'; - } -}