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 cd3b2461fa..d9a2923d52 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; -import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -67,7 +67,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class AssetProfileController extends BaseController { private final TbAssetProfileService tbAssetProfileService; - private final TbImageService tbImageService; + private final ImageService imageService; @ApiOperation(value = "Get Asset Profile (getAssetProfileById)", notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " + @@ -85,7 +85,7 @@ public class AssetProfileController extends BaseController { AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); var result = checkAssetProfileId(assetProfileId, Operation.READ); if (inlineImages) { - tbImageService.inlineImage(result); + imageService.inlineImage(result); } return result; } 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 4e47f52840..502e40e929 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -51,9 +51,9 @@ import org.thingsboard.server.common.data.id.EdgeId; 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.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService; -import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -97,7 +97,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class DashboardController extends BaseController { private final TbDashboardService tbDashboardService; - private final TbImageService tbImageService; + private final ImageService imageService; public static final String DASHBOARD_ID = "dashboardId"; private static final String HOME_DASHBOARD_ID = "homeDashboardId"; @@ -164,7 +164,7 @@ public class DashboardController extends BaseController { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); var result = checkDashboardId(dashboardId, Operation.READ); if (inlineImages) { - tbImageService.inlineImages(result); + imageService.inlineImages(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 a1e51b920c..3c07946309 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -37,10 +37,10 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService; -import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -75,7 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class DeviceProfileController extends BaseController { private final TbDeviceProfileService tbDeviceProfileService; - private final TbImageService tbImageService; + private final ImageService imageService; @Autowired private TimeseriesService timeseriesService; @@ -96,7 +96,7 @@ public class DeviceProfileController extends BaseController { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); var result = checkDeviceProfileId(deviceProfileId, Operation.READ); if (inlineImages) { - tbImageService.inlineImage(result); + imageService.inlineImage(result); } return result; } diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index 1cfdd0347b..7685640929 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -52,13 +52,12 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.resource.ImageCacheKey; +import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.service.resource.TbImageService; 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 javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; 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 97b22b5706..fa18fe4265 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -43,9 +43,9 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; 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.resource.TbImageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -75,7 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ public class WidgetTypeController extends AutoCommitController { private final TbWidgetTypeService tbWidgetTypeService; - private final TbImageService tbImageService; + 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. " + @@ -103,7 +103,7 @@ public class WidgetTypeController extends AutoCommitController { WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); var result = checkWidgetTypeId(widgetTypeId, Operation.READ); if (inlineImages) { - tbImageService.inlineImages(result); + imageService.inlineImages(result); } return result; } @@ -270,7 +270,7 @@ public class WidgetTypeController extends AutoCommitController { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); var result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); if (inlineImages) { - result.forEach(tbImageService::inlineImages); + result.forEach(imageService::inlineImages); } 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 3abcfb1122..1fe203249f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -36,9 +36,9 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService; -import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -69,7 +69,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDL public class WidgetsBundleController extends BaseController { private final TbWidgetsBundleService tbWidgetsBundleService; - private final TbImageService tbImageService; + private final ImageService imageService; private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. "; private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types"; @@ -89,7 +89,7 @@ public class WidgetsBundleController extends BaseController { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); var result = checkWidgetsBundleId(widgetsBundleId, Operation.READ); if (inlineImages) { - tbImageService.inlineImage(result); + imageService.inlineImage(result); } return result; } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index ac074f983b..0336349046 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -82,7 +82,7 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; -import org.thingsboard.server.service.resource.ImageCacheKey; +import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.service.resource.TbImageService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg; 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 3224a13f47..cd69f884ac 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 @@ -16,20 +16,13 @@ package org.thingsboard.server.service.resource; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; 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; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbImageDeleteResult; @@ -39,17 +32,13 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; 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.ImageCacheKey; 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; -import java.util.Iterator; -import java.util.LinkedList; import java.util.Optional; -import java.util.Queue; import java.util.concurrent.TimeUnit; @Service @@ -163,94 +152,4 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb throw e; } } - - public void inlineImage(HasImage entity) { - entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); - } - - @Override - public void inlineImages(Dashboard dashboard) { - inlineImage(dashboard); - inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration()); - } - - @Override - public void inlineImages(WidgetTypeDetails widgetTypeDetails) { - inlineImage(widgetTypeDetails); - inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor()); - } - - private void inlineIntoJson(TenantId tenantId, JsonNode root) { - Queue tasks = new LinkedList<>(); - tasks.add(new JsonNodeProcessingTask("", root)); - while (!tasks.isEmpty()) { - JsonNodeProcessingTask task = tasks.poll(); - 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(); ) { - String childName = it.next(); - JsonNode childValue = on.get(childName); - if (childValue.isTextual()) { - on.put(childName, inlineImage(tenantId, currentPath + childName, childValue.asText())); - } 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++; - } - } - } - } - - private String inlineImage(TenantId tenantId, String path, String url) { - try { - ImageCacheKey key = getKeyFromUrl(tenantId, url); - if (key != null) { - var imageInfo = imageService.getImageInfoByTenantIdAndKey(key.getTenantId(), key.getKey()); - if (imageInfo != null) { - byte[] data = key.isPreview() ? imageService.getImagePreview(tenantId, imageInfo.getId()) : imageService.getImageData(tenantId, imageInfo.getId()); - ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); - return DataConstants.TB_IMAGE_PREFIX + "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data); - } - } - } catch (Exception e) { - log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, url, e); - } - return url; - } - - private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) throws JsonProcessingException { - ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - return preview ? descriptor.getPreviewDescriptor() : descriptor; - } - - private ImageCacheKey getKeyFromUrl(TenantId tenantId, String url) { - if (StringUtils.isBlank(url)) { - return null; - } - TenantId imageTenantId = null; - if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) { - imageTenantId = tenantId; - } else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) { - imageTenantId = TenantId.SYS_TENANT_ID; - } - if (imageTenantId != null) { - var parts = url.split("/"); - if (parts.length == 5) { - return new ImageCacheKey(imageTenantId, parts[4], false); - } else if (parts.length == 6 && "preview".equals(parts[5])) { - return new ImageCacheKey(imageTenantId, parts[4], true); - } - } - return 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 be49ea8872..2bfb9c3dd2 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 @@ -22,6 +22,7 @@ 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.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.resource.ImageCacheKey; public interface TbImageService { @@ -37,10 +38,4 @@ public interface TbImageService { void evictETag(ImageCacheKey imageCacheKey); - void inlineImage(HasImage entity); - - void inlineImages(Dashboard dashboard); - - void inlineImages(WidgetTypeDetails widgetTypeDetails); - } 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 e5dbaecc37..6d561a289d 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 @@ -55,4 +55,10 @@ public interface ImageService { boolean replaceBase64WithImageUrl(HasImage hasImage, String title, String type); boolean replaceBase64WithImageUrl(Dashboard dashboard); boolean replaceBase64WithImageUrl(WidgetTypeDetails widgetType); + + void inlineImage(HasImage entity); + + void inlineImages(Dashboard dashboard); + + void inlineImages(WidgetTypeDetails widgetTypeDetails); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 3abbde8401..5a787f24a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -173,6 +173,10 @@ public class StringUtils { return false; } + public static String substringBeforeLast(String str, String separator) { + return org.apache.commons.lang3.StringUtils.substringBeforeLast(str, separator); + } + public static String substringAfterLast(String str, String sep) { return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); } 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 94dc1a6441..b8d2267663 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 DataConstants.TB_IMAGE_PREFIX + "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git + return "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git } return null; } 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 eb4c135cfc..4bb17f3b8c 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,15 +15,16 @@ */ package org.thingsboard.server.dao.resource; +import com.fasterxml.jackson.core.JsonProcessingException; 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.util.Base64Utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.RegexUtils; import org.thingsboard.server.common.data.Dashboard; @@ -32,6 +33,7 @@ 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.StringUtils; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -56,6 +58,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -236,7 +239,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic TbImageDeleteResult.TbImageDeleteResultBuilder result = TbImageDeleteResult.builder(); boolean success = true; if (!force) { - var link = imageInfo.getLink(); + var link = DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink(); Map>> affectedEntities = new HashMap<>(); imageContainerDaoMap.forEach((entityType, imageContainerDao) -> { var entities = tenantId.isSysTenantId() ? imageContainerDao.findByImageLink(link, MAX_ENTITIES_TO_FIND) : @@ -366,44 +369,67 @@ public class BaseImageService extends BaseResourceService implements ImageServic return base64ToImageUrl(tenantId, name, data, false); } + private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:(.*):(.*);data:(.*);.*"); + 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() + if (StringUtils.isBlank(data)) { + return data; + } + var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data); + boolean matches = matcher.matches(); + String mdResourceKey = null; + String mdResourceName = null; + String mdMediaType; + if (matches) { + mdResourceKey = new String(Base64Utils.decodeFromString(matcher.group(1)), StandardCharsets.UTF_8); + mdResourceName = new String(Base64Utils.decodeFromString(matcher.group(2)), StandardCharsets.UTF_8); + mdMediaType = matcher.group(3); + } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) { + mdMediaType = org.apache.commons.lang3.StringUtils.substringBetween(data, "data:", ";base64"); + } else { + return data; + } + String base64Data = org.apache.commons.lang3.StringUtils.substringAfter(data, "base64,"); + String extension = ImageUtils.mediaTypeToFileExtension(mdMediaType); + 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); + + if (StringUtils.isBlank(mdResourceName)) { + mdResourceName = name; + } + + String fileName; + if (StringUtils.isBlank(mdResourceKey)) { + fileName = mdResourceName.toLowerCase() //TODO: improve to list all the special characters via regexp or similar - .replace("'", "_") - .replace("\"", "_") + .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); + mdResourceKey = fileName + "." + extension; + } else { + fileName = mdResourceKey.split("\\.")[0]; } - return imageInfo.getLink(); - } else { - return data; + + Set existingKeys = findResourceKeysByTenantIdResourceTypeAndKeyPrefix(tenantId, ResourceType.IMAGE, mdResourceKey); + int idx = 1; + while (existingKeys.contains(mdResourceKey)) { + mdResourceKey = fileName + "_(" + idx + ")." + extension; + idx++; + } + image.setResourceKey(mdResourceKey); + image.setTitle(mdResourceName); + image.setFileName(mdResourceKey); + image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType)); + image.setData(imageData); + imageInfo = saveImage(image); } + return DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink(); } private void base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) { @@ -436,4 +462,97 @@ public class BaseImageService extends BaseResourceService implements ImageServic } } } + + @Override + public void inlineImage(HasImage entity) { + entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); + } + + @Override + public void inlineImages(Dashboard dashboard) { + inlineImage(dashboard); + inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration()); + } + + @Override + public void inlineImages(WidgetTypeDetails widgetTypeDetails) { + inlineImage(widgetTypeDetails); + inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor()); + } + + private void inlineIntoJson(TenantId tenantId, JsonNode root) { + Queue tasks = new LinkedList<>(); + tasks.add(new JsonNodeProcessingTask("", 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, inlineImage(tenantId, currentPath + childName, childValue.asText())); + } 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++; + } + } + } + } + + private String inlineImage(TenantId tenantId, String path, String url) { + try { + ImageCacheKey key = getKeyFromUrl(tenantId, url); + if (key != null) { + var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getKey()); + if (imageInfo != null) { + byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); + ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); + String tbImagePrefix = "tb-image:" + Base64Utils.encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":" + + Base64Utils.encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ";"; + return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data); + } + } + } catch (Exception e) { + log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, url, e); + } + return url; + } + + private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) throws JsonProcessingException { + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + return preview ? descriptor.getPreviewDescriptor() : descriptor; + } + + private ImageCacheKey getKeyFromUrl(TenantId tenantId, String url) { + if (org.thingsboard.server.common.data.StringUtils.isBlank(url)) { + return null; + } + TenantId imageTenantId = null; + if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) { + imageTenantId = tenantId; + } else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) { + imageTenantId = TenantId.SYS_TENANT_ID; + } + if (imageTenantId != null) { + var parts = url.split("/"); + if (parts.length == 5) { + return new ImageCacheKey(imageTenantId, parts[4], false); + } else if (parts.length == 6 && "preview".equals(parts[5])) { + return new ImageCacheKey(imageTenantId, parts[4], true); + } + } + return null; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java rename to dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java index 55ddb25dec..7ee2f4a6b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/ImageCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.resource; +package org.thingsboard.server.dao.resource; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId;