From d6bfc40dc65f1356a6ef0653efd7380b25468bcc Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 29 Nov 2023 13:15:07 +0200 Subject: [PATCH] Automatic image import-export from dashboards and widget types --- .../main/data/upgrade/3.6.1/schema_update.sql | 4 +- .../controller/AssetProfileController.java | 2 +- .../controller/DeviceProfileController.java | 2 +- .../controller/WidgetsBundleController.java | 2 +- .../dashboard/DefaultTbDashboardService.java | 1 + .../type/DefaultWidgetTypeService.java | 3 + .../resource/DefaultTbImageService.java | 29 +-- .../service/resource/TbImageService.java | 2 +- .../server/dao/resource/ImageService.java | 12 +- .../server/dao/resource/ResourceService.java | 3 + .../server/common/data/DashboardInfo.java | 2 +- .../server/common/data/DataConstants.java | 1 + .../server/common/data/TbResourceInfo.java | 2 +- .../common/data/widget/WidgetTypeDetails.java | 3 +- .../dao/asset/AssetProfileServiceImpl.java | 5 + .../dao/dashboard/DashboardServiceImpl.java | 7 + .../dao/device/DeviceProfileServiceImpl.java | 5 + .../dao/model/sql/TbResourceInfoEntity.java | 6 +- .../server/dao/resource/BaseImageService.java | 224 +++++++++++++++++- .../dao/resource/BaseResourceService.java | 9 + .../server/dao/resource/TbResourceDao.java | 2 + .../dao/resource/TbResourceInfoDao.java | 1 + .../dao/sql/resource/JpaTbResourceDao.java | 7 + .../sql/resource/JpaTbResourceInfoDao.java | 7 +- .../resource/TbResourceInfoRepository.java | 11 +- .../sql/resource/TbResourceRepository.java | 4 + .../server/dao/util/ImageUtils.java | 8 +- .../dao/util/JsonNodeProcessingTask.java | 30 +++ .../dao/util/JsonPathProcessingTask.java | 70 ++++++ .../dao/widget/WidgetTypeServiceImpl.java | 5 + .../dao/widget/WidgetsBundleServiceImpl.java | 5 + .../resources/sql/schema-entities-idx.sql | 2 + 32 files changed, 433 insertions(+), 43 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java diff --git a/application/src/main/data/upgrade/3.6.1/schema_update.sql b/application/src/main/data/upgrade/3.6.1/schema_update.sql index 19362bb38e..ebcf20a2c5 100644 --- a/application/src/main/data/upgrade/3.6.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.6.1/schema_update.sql @@ -30,6 +30,8 @@ $$; ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar; ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea; -ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid +ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid; + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag); -- RESOURCES UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index f11f4c5f64..cd3b2461fa 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -85,7 +85,7 @@ public class AssetProfileController extends BaseController { AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); var result = checkAssetProfileId(assetProfileId, Operation.READ); if (inlineImages) { - tbImageService.inlineImages(result); + tbImageService.inlineImage(result); } return result; } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index e1489ec2ad..a1e51b920c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -96,7 +96,7 @@ public class DeviceProfileController extends BaseController { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); var result = checkDeviceProfileId(deviceProfileId, Operation.READ); if (inlineImages) { - tbImageService.inlineImages(result); + tbImageService.inlineImage(result); } return result; } diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index bc9fc144dd..3abcfb1122 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -89,7 +89,7 @@ public class WidgetsBundleController extends BaseController { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); var result = checkWidgetsBundleId(widgetsBundleId, Operation.READ); if (inlineImages) { - tbImageService.inlineImages(result); + tbImageService.inlineImage(result); } return result; } 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 70918486a3..8d143d7607 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 @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EdgeId; 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.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; 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 1d40e9630c..53afc9fb4f 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy.widgets.type; import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -33,6 +35,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @AllArgsConstructor public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService { + private final WidgetTypeService widgetTypeService; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java index 9464e950e4..3224a13f47 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java @@ -27,6 +27,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.Base64Utils; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.ImageDescriptor; @@ -40,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.widget.WidgetTypeDetails; import org.thingsboard.server.dao.resource.ImageService; +import org.thingsboard.server.dao.util.JsonNodeProcessingTask; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -162,20 +164,19 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb } } - @Override - public void inlineImages(HasImage entity) { + public void inlineImage(HasImage entity) { entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); } @Override public void inlineImages(Dashboard dashboard) { - inlineImages((HasImage) dashboard); + inlineImage(dashboard); inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration()); } @Override public void inlineImages(WidgetTypeDetails widgetTypeDetails) { - inlineImages((HasImage) widgetTypeDetails); + inlineImage(widgetTypeDetails); inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor()); } @@ -184,8 +185,8 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb tasks.add(new JsonNodeProcessingTask("", root)); while (!tasks.isEmpty()) { JsonNodeProcessingTask task = tasks.poll(); - JsonNode node = task.node; - String currentPath = StringUtils.isBlank(task.path) ? "" : (task.path + "."); + JsonNode node = task.getNode(); + String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + "."); if (node.isObject()) { ObjectNode on = (ObjectNode) node; for (Iterator it = on.fieldNames(); it.hasNext(); ) { @@ -210,16 +211,6 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb } } - private static class JsonNodeProcessingTask { - private final String path; - private final JsonNode node; - - public JsonNodeProcessingTask(String path, JsonNode node) { - this.path = path; - this.node = node; - } - } - private String inlineImage(TenantId tenantId, String path, String url) { try { ImageCacheKey key = getKeyFromUrl(tenantId, url); @@ -228,7 +219,7 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb if (imageInfo != null) { byte[] data = key.isPreview() ? imageService.getImagePreview(tenantId, imageInfo.getId()) : imageService.getImageData(tenantId, imageInfo.getId()); ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); - return "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data); + return DataConstants.TB_IMAGE_PREFIX + "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data); } } } catch (Exception e) { @@ -247,9 +238,9 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb return null; } TenantId imageTenantId = null; - if (url.startsWith("/api/images/tenant/")) { + if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) { imageTenantId = tenantId; - } else if (url.startsWith("/api/images/system/")) { + } else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) { imageTenantId = TenantId.SYS_TENANT_ID; } if (imageTenantId != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java index 25b6417e2f..be49ea8872 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java @@ -37,7 +37,7 @@ public interface TbImageService { void evictETag(ImageCacheKey imageCacheKey); - void inlineImages(HasImage entity); + void inlineImage(HasImage entity); void inlineImages(Dashboard dashboard); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java index 82779fca21..e5dbaecc37 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.dao.resource; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -22,12 +25,14 @@ 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.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import java.util.List; public interface ImageService { - TbResourceInfo saveImage(TbResource image) throws Exception; + TbResourceInfo saveImage(TbResource image); TbResourceInfo saveImageInfo(TbResourceInfo imageInfo); @@ -45,4 +50,9 @@ public interface ImageService { List findSimilarImagesByTenantIdAndKeyStartingWith(TenantId tenantId, byte[] data, String imageKeyStartingWith); + TbResourceInfo findImageByTenantIdAndEtag(TenantId tenantId, String etag); + + boolean replaceBase64WithImageUrl(HasImage hasImage, String title, String type); + boolean replaceBase64WithImageUrl(Dashboard dashboard); + boolean replaceBase64WithImageUrl(WidgetTypeDetails widgetType); } 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 b4cda2d733..4cfc7f5963 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 @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; +import java.util.Set; public interface ResourceService extends EntityDaoService { @@ -54,6 +55,8 @@ public interface ResourceService extends EntityDaoService { PageData findTenantResourcesByResourceTypeAndPageLink(TenantId tenantId, ResourceType lwm2mModel, PageLink pageLink); + Set findResourceKeysByTenantIdResourceTypeAndKeyPrefix(TenantId tenantId, ResourceType resourceType, String key); + void deleteResource(TenantId tenantId, TbResourceId resourceId); void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index fe08934f79..66d81f1579 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -30,7 +30,7 @@ import java.util.Objects; import java.util.Set; @ApiModel -public class DashboardInfo extends BaseData implements HasName, HasTenantId, HasTitle { +public class DashboardInfo extends BaseData implements HasName, HasTenantId, HasTitle, HasImage { private TenantId tenantId; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 4a84743283..ca48ef364a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -50,6 +50,7 @@ public class DataConstants { public static final String HTTP_TRANSPORT_NAME = "HTTP"; public static final String SNMP_TRANSPORT_NAME = "SNMP"; public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!"; + public static final String TB_IMAGE_PREFIX = "tb-image;"; public static final String[] allScopes() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index b8d2267663..94dc1a6441 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -109,7 +109,7 @@ public class TbResourceInfo extends BaseData implements HasName, H @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getLink() { if (resourceType == ResourceType.IMAGE) { - return "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git + return DataConstants.TB_IMAGE_PREFIX + "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git } return null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index 29adde9cc4..b335038b54 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java @@ -21,6 +21,7 @@ import lombok.Data; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -29,7 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" }) -public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, ExportableEntity { +public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Length(fieldName = "image", max = 1000000) @ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 7cc251f9ec..884d30e317 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -71,6 +72,9 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService assetProfileValidator; + @Autowired + private ImageService imageService; + @TransactionalEventListener(classes = AssetProfileEvictEvent.class) @Override public void handleEvictEvent(AssetProfileEvictEvent event) { @@ -141,6 +145,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService dashboardValidator; @@ -153,6 +157,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); } try { + imageService.replaceBase64WithImageUrl(dashboard); var saved = dashboardDao.save(dashboard.getTenantId(), dashboard); publishEvictEvent(new DashboardTitleEvictEvent(saved.getId())); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId()) @@ -170,6 +175,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } + + @Override public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId) { Dashboard dashboard = findDashboardById(tenantId, dashboardId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 7f6dec2740..529d926a12 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -47,6 +47,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -91,6 +92,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService implemen private String searchText; @Column(name = RESOURCE_ETAG_COLUMN) - private String hashCode; + private String etag; @Column(name = RESOURCE_FILE_NAME_COLUMN) private String fileName; @@ -92,7 +92,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen this.resourceType = resource.getResourceType().name(); this.resourceKey = resource.getResourceKey(); this.searchText = resource.getSearchText(); - this.hashCode = resource.getEtag(); + this.etag = resource.getEtag(); this.fileName = resource.getFileName(); this.descriptor = resource.getDescriptor(); this.externalId = getUuid(resource.getExternalId()); @@ -107,7 +107,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceKey(resourceKey); resource.setSearchText(searchText); - resource.setEtag(hashCode); + resource.setEtag(etag); resource.setFileName(fileName); resource.setDescriptor(descriptor); resource.setExternalId(getEntityId(externalId, TbResourceId::new)); 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 fa06ba6e95..eb4c135cfc 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 @@ -15,13 +15,21 @@ */ 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.hash.Hashing; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.RegexUtils; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbImageDeleteResult; @@ -33,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.ImageContainerDao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; @@ -41,14 +50,21 @@ 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.JsonNodeProcessingTask; +import org.thingsboard.server.dao.util.JsonPathProcessingTask; import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; import javax.annotation.PostConstruct; +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.Objects; +import java.util.Queue; import java.util.Set; import java.util.regex.Pattern; @@ -57,12 +73,38 @@ import java.util.regex.Pattern; public class BaseImageService extends BaseResourceService implements ImageService { private static final int MAX_ENTITIES_TO_FIND = 10; + + public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); + public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); + + static { + DASHBOARD_BASE64_MAPPING.put("settings.dashboardLogoUrl", "$prefix logo"); + DASHBOARD_BASE64_MAPPING.put("states.default.layouts.main.gridSettings.backgroundImageUrl", "$prefix background"); + DASHBOARD_BASE64_MAPPING.put("states.default.layouts.right.gridSettings.backgroundImageUrl", "$prefix right background"); + DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.main.gridSettings.backgroundImageUrl", "$prefix $stateId background"); + DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.right.gridSettings.backgroundImageUrl", "$prefix $stateId right background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.backgroundImageUrl", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.mapImageUrl", "$prefix widget \"$title\" map image"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImage", "$prefix widget \"$title\" marker image"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImages", "$prefix widget \"$title\" marker image $index"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageUrl", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageBase64", "$prefix widget \"$title\" background"); + DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].datasources.*.dataKeys.*.settings.customIcon", "$prefix widget \"$title\" custom icon"); + + WIDGET_TYPE_BASE64_MAPPING.put("settings.backgroundImageUrl", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.mapImageUrl", "$prefix map image"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImage", "Map marker image"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImages", "Map marker image $index"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageUrl", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageBase64", "$prefix background"); + WIDGET_TYPE_BASE64_MAPPING.put("datasources.*.dataKeys.*.settings.customIcon", "$prefix custom icon"); + } + private final AssetProfileDao assetProfileDao; private final DeviceProfileDao deviceProfileDao; private final WidgetsBundleDao widgetsBundleDao; private final WidgetTypeDao widgetTypeDao; private final DashboardInfoDao dashboardInfoDao; - private final Map> imageContainerDaoMap = new HashMap<>(); public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, @@ -86,9 +128,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic } - @Transactional @Override - public TbResourceInfo saveImage(TbResource image) throws Exception { + @SneakyThrows + public TbResourceInfo saveImage(TbResource image) { if (image.getId() == null && StringUtils.isEmpty(image.getResourceKey())) { image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName())); } @@ -220,4 +262,178 @@ public class BaseImageService extends BaseResourceService implements ImageServic return resourceInfoDao.findByTenantIdAndEtagAndKeyStartingWith(tenantId, etag, imageKeyStartingWith); } + @Override + public TbResourceInfo findImageByTenantIdAndEtag(TenantId tenantId, String etag) { + return resourceInfoDao.findByTenantIdAndEtag(tenantId, ResourceType.IMAGE, etag); + } + + @Override + public boolean replaceBase64WithImageUrl(HasImage entity, String title, String type) { + entity.setImage(base64ToImageUrl(entity.getTenantId(), "\"" + title + "\" " + type + " image", entity.getImage())); + return true; //TODO: should return true only if something is changed. + } + + @Override + public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) { + String prefix = "\"" + entity.getName() + "\" widget"; // TODO: "system widget" if SYS TENANT ID; + entity.setImage(base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage())); + if (entity.getDescriptor().isObject()) { + ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); + JsonNode defaultConfig = JacksonUtil.toJsonNode(descriptor.get("defaultConfig").asText()); + base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); + descriptor.put("defaultConfig", defaultConfig.toString()); + } + base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); + return true; //TODO: should return true only if something is changed. + } + + @Override + public boolean replaceBase64WithImageUrl(Dashboard entity) { + String prefix = "\"" + entity.getTitle() + "\" dashboard"; + entity.setImage(base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage())); + base64ToImageUrlUsingMapping(entity.getTenantId(), DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), entity.getConfiguration()); + base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getConfiguration()); + return true; //TODO: should return true only if something is changed. + } + + private void base64ToImageUrlUsingMapping(TenantId tenantId, Map mapping, Map templateParams, JsonNode configuration) { + 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 (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 = org.apache.commons.lang3.StringUtils.substringBetween(token, "[$", "]"); + token = org.apache.commons.lang3.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 replacements : task.getVariables().entrySet()) { + name = name.replace("$" + replacements.getKey(), replacements.getValue()); + } + if (node.isObject() && value.isTextual()) { + ((ObjectNode) node).put(token, base64ToImageUrl(tenantId, 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, base64ToImageUrl(tenantId, arrayElementName, array.get(i).asText())); + } + } + } else { + if (org.thingsboard.server.common.data.StringUtils.isNotEmpty(variableName) && org.thingsboard.server.common.data.StringUtils.isNotEmpty(variableValue)) { + tasks.add(task.next(value, variableName, variableValue)); + } else { + tasks.add(task.next(value)); + } + } + } + } + } + } + } + + private String base64ToImageUrl(TenantId tenantId, String name, String data) { + return base64ToImageUrl(tenantId, name, data, false); + } + + private String base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) { + if (org.thingsboard.server.common.data.StringUtils.isNotBlank(data) && + (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") + || (!strict && data.startsWith("data:image/")))) { + String base64Data = org.apache.commons.lang3.StringUtils.substringAfter(data, "base64,"); + String mediaType = org.apache.commons.lang3.StringUtils.substringBetween(data, "data:", ";base64"); + String extension = ImageUtils.mediaTypeToFileExtension(mediaType); + byte[] imageData = Base64.getDecoder().decode(base64Data); + String etag = Hashing.sha256().hashBytes(imageData).toString(); + var imageInfo = findImageByTenantIdAndEtag(tenantId, etag); + if (imageInfo == null) { + TbResource image = new TbResource(); + image.setTenantId(tenantId); + image.setResourceType(ResourceType.IMAGE); + String fileName = name.toLowerCase() + //TODO: improve to list all the special characters via regexp or similar + .replace("'", "_") + .replace("\"", "_") + .replace(" ", "_") + .replace("/", "_"); + String key = fileName + "." + extension; + Set existingKeys = findResourceKeysByTenantIdResourceTypeAndKeyPrefix(tenantId, ResourceType.IMAGE, key); + int idx = 1; + while (existingKeys.contains(key)) { + key = fileName + " (" + idx + ")." + extension; + idx++; + } + image.setResourceKey(key); + image.setTitle(name); + image.setFileName(key); + image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mediaType)); + image.setData(imageData); + imageInfo = saveImage(image); + } + return imageInfo.getLink(); + } else { + return data; + } + } + + private void base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) { + Queue tasks = new LinkedList<>(); + tasks.add(new JsonNodeProcessingTask(title, root)); + while (!tasks.isEmpty()) { + JsonNodeProcessingTask task = tasks.poll(); + JsonNode node = task.getNode(); + String currentPath = org.thingsboard.server.common.data.StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + " "); + if (node.isObject()) { + ObjectNode on = (ObjectNode) node; + for (Iterator it = on.fieldNames(); it.hasNext(); ) { + String childName = it.next(); + JsonNode childValue = on.get(childName); + if (childValue.isTextual()) { + on.put(childName, base64ToImageUrl(tenantId, currentPath + childName, childValue.asText(), true)); + } else if (childValue.isObject() || childValue.isArray()) { + tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue)); + } + } + } else if (node.isArray()) { + ArrayNode childArray = (ArrayNode) node; + int i = 0; + for (JsonNode element : childArray) { + if (element.isObject()) { + tasks.add(new JsonNodeProcessingTask(currentPath + " " + i, element)); + } + i++; + } + } + } + } } 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 d935409afb..b9aa0b100d 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 @@ -44,8 +44,10 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -197,6 +199,13 @@ public class BaseResourceService extends AbstractCachedEntityService findResourceKeysByTenantIdResourceTypeAndKeyPrefix(TenantId tenantId, ResourceType resourceType, String key) { + log.trace("Executing findResourceKeysByTenantIdAndPrefix [{}][{}]", tenantId, key); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return resourceDao.findResourceKeysByTenantIdResourceTypeAndKeyPrefix(tenantId, resourceType, key); + } + @Override public void deleteResourcesByTenantId(TenantId tenantId) { log.trace("Executing deleteResourcesByTenantId, tenantId [{}]", tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 8d0ce7fb35..f2ca16c313 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -26,6 +26,7 @@ import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; import java.util.List; +import java.util.Set; public interface TbResourceDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { @@ -46,4 +47,5 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId); + Set findResourceKeysByTenantIdResourceTypeAndKeyPrefix(TenantId tenantId, ResourceType resourceType, String keyPrefix); } 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 3c9b172657..237c7f44dd 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 @@ -39,4 +39,5 @@ public interface TbResourceInfoDao extends Dao { List findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query); + TbResourceInfo findByTenantIdAndEtag(TenantId tenantId, ResourceType resourceType, String etag); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 370b21a90e..08cf3dd860 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -31,7 +31,9 @@ import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.UUID; @Slf4j @@ -125,6 +127,11 @@ public class JpaTbResourceDao extends JpaAbstractDao findResourceKeysByTenantIdResourceTypeAndKeyPrefix(TenantId tenantId, ResourceType resourceType, String keyPrefix) { + return new HashSet<>(resourceRepository.findResourceKeys(tenantId.getId(), resourceType.name(), keyPrefix + "%")); + } + @Override public TbResourceId getExternalIdByInternal(TbResourceId internalId) { return DaoUtil.toEntityId(resourceRepository.getExternalIdByInternal(internalId.getId()), TbResourceId::new); 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 5617aa997c..71696dd2f4 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ResourceType; @@ -102,7 +103,11 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) { - return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndHashCodeAndResourceKeyStartingWith(tenantId.getId(), etag, query)); + return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndEtagAndResourceKeyStartingWith(tenantId.getId(), etag, query)); } + @Override + public TbResourceInfo findByTenantIdAndEtag(TenantId tenantId, ResourceType resourceType, String etag) { + return DaoUtil.getData(resourceInfoRepository.findByTenantIdAndEtag(TenantId.SYS_TENANT_ID.getId(), tenantId.getId(), resourceType.name(), etag)); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index 0a0f7ff127..4ebdd1f10c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -30,7 +30,7 @@ public interface TbResourceInfoRepository extends JpaRepository findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId, - @Param("systemAdminId") UUID sysadminId, + @Param("systemTenantId") UUID systemTenantId, @Param("resourceTypes") List resourceTypes, @Param("searchText") String searchText, Pageable pageable); @@ -62,6 +62,11 @@ public interface TbResourceInfoRepository extends JpaRepository findByTenantIdAndHashCodeAndResourceKeyStartingWith(UUID tenantId, String hashCode, String query); + List findByTenantIdAndEtagAndResourceKeyStartingWith(UUID tenantId, String etag, String query); + @Query(value = "SELECT * FROM resource r WHERE (r.tenant_id = :systemTenantId OR r.tenant_id = :tenantId) AND r.resource_type = :resourceType AND r.etag = :etag LIMIT 1", nativeQuery = true) + TbResourceInfoEntity findByTenantIdAndEtag(@Param("systemTenantId") UUID sysTenantId, + @Param("tenantId") UUID tenantId, + @Param("resourceType") String resourceType, + @Param("etag") String etag); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index b321b1289f..f6af27ab58 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -94,4 +94,8 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT resourceKey FROM TbResourceInfoEntity WHERE tenantId = :tenantId AND resourceType = :resourceType AND resourceKey LIKE :resourceKeyPrefix ") + List findResourceKeys(@Param("tenantId")UUID tenantId, + @Param("resourceType") String resourceType, + @Param("resourceKeyPrefix") String resourceKeyPrefix); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index 58c03258c8..104a2cf6ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -110,15 +110,15 @@ public class ImageUtils { String strWidth = document.getDocumentElement().getAttribute("width"); String strHeight = document.getDocumentElement().getAttribute("height"); if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) { - width = Integer.parseInt(strWidth); - height = Integer.parseInt(strHeight); + width = (int) Double.parseDouble(strWidth); + height = (int) Double.parseDouble(strHeight);; } else { String viewBox = document.getDocumentElement().getAttribute("viewBox"); if (StringUtils.isNotEmpty(viewBox)) { String[] viewBoxValues = viewBox.split(" "); if (viewBoxValues.length > 3) { - width = Integer.parseInt(viewBoxValues[2]); - height = Integer.parseInt(viewBoxValues[3]); + width = (int) Double.parseDouble(viewBoxValues[2]); + height = (int) Double.parseDouble(viewBoxValues[3]); } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java b/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java new file mode 100644 index 0000000000..17454b4757 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/JsonNodeProcessingTask.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class JsonNodeProcessingTask { + private final String path; + private final JsonNode node; + + public JsonNodeProcessingTask(String path, JsonNode node) { + this.path = path; + this.node = node; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java b/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java new file mode 100644 index 0000000000..c8703b6155 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/JsonPathProcessingTask.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.util; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.dao.dashboard.DashboardServiceImpl; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +@Data +public 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/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 975e0cc9a1..26a3924467 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundleWidget; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -64,6 +65,9 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { @Autowired protected ApplicationEventPublisher eventPublisher; + @Autowired + protected ImageService imageService; + @Override public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) { log.trace("Executing findWidgetTypeById [{}]", widgetTypeId); @@ -90,6 +94,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { log.trace("Executing saveWidgetType [{}]", widgetTypeDetails); widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId); try { + imageService.replaceBase64WithImageUrl(widgetTypeDetails); WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) .entityId(result.getId()).added(widgetTypeDetails.getId() == null).build()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 82acae9fff..a2fcf6b374 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -31,6 +31,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -59,6 +60,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Autowired protected ApplicationEventPublisher eventPublisher; + @Autowired + private ImageService imageService; + @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); @@ -71,6 +75,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { log.trace("Executing saveWidgetsBundle [{}]", widgetsBundle); widgetsBundleValidator.validate(widgetsBundle, WidgetsBundle::getTenantId); try { + imageService.replaceBase64WithImageUrl(widgetsBundle, widgetsBundle.getName(), "bundle"); WidgetsBundle result = widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId()) .entityId(result.getId()).added(widgetsBundle.getId() == null).build()); diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 68eede5ce4..472c0f601e 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -119,3 +119,5 @@ CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notificat CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC); CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ'; + +CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);