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 47a7eede4b..f11f4c5f64 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; 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; @@ -45,6 +46,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFIL import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_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.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -64,6 +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; @ApiOperation(value = "Get Asset Profile (getAssetProfileById)", notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " + @@ -74,10 +78,16 @@ public class AssetProfileController extends BaseController { @ResponseBody public AssetProfile getAssetProfileById( @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(ASSET_PROFILE_ID, strAssetProfileId); AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); - return checkAssetProfileId(assetProfileId, Operation.READ); + var result = checkAssetProfileId(assetProfileId, Operation.READ); + if (inlineImages) { + tbImageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)", 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 ee24c7755c..5c1842b4b4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -32,6 +32,9 @@ public class ControllerConstants { protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "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 afc737ab88..4e47f52840 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -53,6 +53,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; 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; @@ -74,6 +75,8 @@ 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; @@ -94,6 +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; public static final String DASHBOARD_ID = "dashboardId"; private static final String HOME_DASHBOARD_ID = "homeDashboardId"; @@ -153,10 +157,16 @@ public class DashboardController extends BaseController { @ResponseBody public Dashboard getDashboardById( @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + @PathVariable(DASHBOARD_ID) String strDashboardId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); - return checkDashboardId(dashboardId, Operation.READ); + var result = checkDashboardId(dashboardId, Operation.READ); + if (inlineImages) { + tbImageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", 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 c888d67252..e1489ec2ad 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.page.PageLink; 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; @@ -52,6 +53,8 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_TEXT_SEARCH_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.NEW_LINE; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; @@ -72,6 +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; @Autowired private TimeseriesService timeseriesService; @@ -85,10 +89,16 @@ public class DeviceProfileController extends BaseController { @ResponseBody public DeviceProfile getDeviceProfileById( @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) - @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); - return checkDeviceProfileId(deviceProfileId, Operation.READ); + var result = checkDeviceProfileId(deviceProfileId, Operation.READ); + if (inlineImages) { + tbImageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)", 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 8d475a211d..f64ca1da00 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.model.ModelConstants; 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; @@ -53,6 +54,8 @@ import java.util.Collections; import java.util.List; 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; @@ -72,6 +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 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. " + @@ -92,10 +96,16 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public WidgetTypeDetails getWidgetTypeById( @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + @PathVariable("widgetTypeId") String strWidgetTypeId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); - return checkWidgetTypeId(widgetTypeId, Operation.READ); + var result = checkWidgetTypeId(widgetTypeId, Operation.READ); + if (inlineImages) { + tbImageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)", @@ -253,9 +263,16 @@ public class WidgetTypeController extends AutoCommitController { @ResponseBody public List getBundleWidgetTypesDetails( @ApiParam(value = "Widget Bundle Id", required = true) - @RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + @RequestParam("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages + ) throws ThingsboardException { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + var result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); + if (inlineImages) { + result.forEach(tbImageService::inlineImages); + } + return result; } @ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)", 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 f62d1b7b05..bc9fc144dd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetsBundle; 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; @@ -47,6 +48,8 @@ import java.util.List; import java.util.Set; 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; @@ -66,6 +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 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"; @@ -78,10 +82,16 @@ public class WidgetsBundleController extends BaseController { @ResponseBody public WidgetsBundle getWidgetsBundleById( @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + @PathVariable("widgetsBundleId") String strWidgetsBundleId, + @ApiParam(value = INLINE_IMAGES_DESCRIPTION) + @RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException { checkParameter("widgetsBundleId", strWidgetsBundleId); WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); - return checkWidgetsBundleId(widgetsBundleId, Operation.READ); + var result = checkWidgetsBundleId(widgetsBundleId, Operation.READ); + if (inlineImages) { + tbImageService.inlineImages(result); + } + return result; } @ApiOperation(value = "Create Or Update Widget Bundle (saveWidgetsBundle)", @@ -196,9 +206,9 @@ public class WidgetsBundleController extends BaseController { } else { TenantId tenantId = getCurrentUser().getTenantId(); if (tenantOnly != null && tenantOnly) { - return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); } else { - return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); + return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink)); } } } 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 822b46bc1d..562eea0e5a 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,12 +16,18 @@ 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.data.util.Pair; 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.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ImageDescriptor; import org.thingsboard.server.common.data.StringUtils; @@ -29,31 +35,40 @@ import org.thingsboard.server.common.data.TbImageDeleteResult; 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.asset.AssetProfile; 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.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.resource.ImageService; 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.Map; import java.util.Optional; +import java.util.Queue; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; @Service +@Slf4j @TbCoreComponent public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService { private final TbClusterService clusterService; private final ImageService imageService; - private final Cache cache; + private final Cache etagCache; public DefaultTbImageService(TbClusterService clusterService, ImageService imageService, @Value("${cache.image.etag.timeToLiveInMinutes:44640}") int cacheTtl, @Value("${cache.image.etag.maxSize:10000}") int cacheMaxSize) { this.clusterService = clusterService; this.imageService = imageService; - this.cache = Caffeine.newBuilder() + this.etagCache = Caffeine.newBuilder() .expireAfterAccess(cacheTtl, TimeUnit.MINUTES) .maximumSize(cacheMaxSize) .build(); @@ -61,17 +76,17 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb @Override public String getETag(ImageCacheKey imageCacheKey) { - return cache.getIfPresent(imageCacheKey); + return etagCache.getIfPresent(imageCacheKey); } @Override public void putETag(ImageCacheKey imageCacheKey, String etag) { - cache.put(imageCacheKey, etag); + etagCache.put(imageCacheKey, etag); } @Override public void evictETag(ImageCacheKey imageCacheKey) { - cache.invalidate(imageCacheKey); + etagCache.invalidate(imageCacheKey); } @Override @@ -151,4 +166,116 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb } } + @Override + public void inlineImages(Dashboard entity) { + var tenantId = entity.getTenantId(); + entity.setImage(inlineImage(tenantId, "image", entity.getImage())); + inlineIntoJson(tenantId, entity.getConfiguration()); + } + + @Override + public void inlineImages(WidgetTypeDetails entity) { + var tenantId = entity.getTenantId(); + entity.setImage(inlineImage(tenantId, "image", entity.getImage())); + inlineIntoJson(tenantId, entity.getDescriptor()); + } + + @Override + public void inlineImages(WidgetsBundle entity) { + entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); + } + + @Override + public void inlineImages(AssetProfile entity) { + entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); + } + + @Override + public void inlineImages(DeviceProfile entity) { + entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage())); + } + + 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.node; + String currentPath = StringUtils.isBlank(task.path) ? "" : (task.path + "."); + 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 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); + 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 "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("/api/images/tenant/")) { + imageTenantId = tenantId; + } else if (url.startsWith("/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 adcf3736f1..d830a3a921 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 @@ -15,11 +15,16 @@ */ package org.thingsboard.server.service.resource; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TbImageDeleteResult; 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.asset.AssetProfile; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.common.data.widget.WidgetsBundle; public interface TbImageService { @@ -34,4 +39,14 @@ public interface TbImageService { void putETag(ImageCacheKey imageCacheKey, String etag); void evictETag(ImageCacheKey imageCacheKey); + + void inlineImages(Dashboard entity); + + void inlineImages(WidgetTypeDetails entity); + + void inlineImages(WidgetsBundle entity); + + void inlineImages(AssetProfile entity); + + void inlineImages(DeviceProfile entity); } 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 424e0bc6bc..29adde9cc4 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 @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.validation.NoXss; public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, ExportableEntity { @Length(fieldName = "image", max = 1000000) - @ApiModelProperty(position = 9, value = "Base64 encoded thumbnail") + @ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") private String image; @NoXss @Length(fieldName = "description", max = 1024) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index 9096207abe..922bb72723 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -60,12 +60,13 @@ public class WidgetsBundle extends BaseData implements HasName, @Length(fieldName = "image", max = 1000000) @Getter @Setter - @ApiModelProperty(position = 6, value = "Base64 encoded thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 6, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String image; @NoXss @Length(fieldName = "description", max = 1024) @Getter + @Setter @ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String description;