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 0c04e586ef..cd1116a420 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -197,7 +197,7 @@ public class ImageController extends BaseController { @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true) @PathVariable String key) throws Exception { TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); - return tbImageService.exportImage(imageInfo); + return imageService.exportImage(imageInfo); } @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") 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 8f5226e8b0..dd1bb2b6ff 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 @@ -186,23 +186,6 @@ public class DefaultTbImageService extends AbstractTbEntityService implements Tb } } - @Override - public ResourceExportData exportImage(TbResourceInfo imageInfo) { - ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); - byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId()); - return ResourceExportData.builder() - .mediaType(descriptor.getMediaType()) - .fileName(imageInfo.getFileName()) - .title(imageInfo.getTitle()) - .type(ResourceType.IMAGE) - .subType(imageInfo.getResourceSubType()) - .resourceKey(imageInfo.getResourceKey()) - .isPublic(imageInfo.isPublic()) - .publicResourceKey(imageInfo.getPublicResourceKey()) - .data(Base64.getEncoder().encodeToString(data)) - .build(); - } - @Override public TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception { TbResource image = new TbResource(); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index d95aa97497..34082d2cc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -172,29 +172,16 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements return resources.values().stream() .map(resourceInfo -> { if (resourceInfo.getResourceType() == ResourceType.IMAGE) { - ResourceExportData imageExportData = tbImageService.exportImage(resourceInfo); + ResourceExportData imageExportData = imageService.exportImage(resourceInfo); imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import return imageExportData; } else { - return exportResource(resourceInfo); + return resourceService.exportResource(resourceInfo); } }) .toList(); } - private ResourceExportData exportResource(TbResourceInfo resourceInfo) { - byte[] data = resourceService.getResourceData(resourceInfo.getTenantId(), resourceInfo.getId()); - return ResourceExportData.builder() - .mediaType(resourceInfo.getResourceType().getMediaType()) - .fileName(resourceInfo.getFileName()) - .title(resourceInfo.getTitle()) - .type(resourceInfo.getResourceType()) - .subType(resourceInfo.getResourceSubType()) - .resourceKey(resourceInfo.getResourceKey()) - .data(Base64.getEncoder().encodeToString(data)) - .build(); - } - private TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException { if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE || exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) { 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 56716f79b7..8e4daf40e2 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,8 +37,6 @@ public interface TbImageService { void evictETags(ImageCacheKey imageCacheKey); - ResourceExportData exportImage(TbResourceInfo imageInfo); - TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception; } diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 94ebc2ba14..27fc2da4b7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -618,7 +618,7 @@ public class DashboardControllerTest extends AbstractControllerTest { + Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":" + imageInfo.getEtag() + ";data:image/png;base64,"); String resourceRef = exportedDashboard.getConfiguration().get("widgets").get("xxx").get("config") - .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").get("id").asText(); + .get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText(); assertThat(resourceRef).isEqualTo("tb-resource:" + Base64.getEncoder().encodeToString(resourceInfo.getResourceType().name().getBytes()) + ":" + resourceInfo.getEtag()); @@ -627,10 +627,12 @@ public class DashboardControllerTest extends AbstractControllerTest { assertThat(resources.get(ResourceType.IMAGE)).singleElement().satisfies(exportedImage -> { assertThat(exportedImage.getFileName()).isEqualTo(imageInfo.getResourceKey()); assertThat(exportedImage.getData()).isEqualTo(Base64.getEncoder().encodeToString(ImageControllerTest.PNG_IMAGE)); + assertThat(exportedImage.getEtag()).isEqualTo(imageInfo.getEtag()); }); assertThat(resources.get(ResourceType.JS_MODULE)).singleElement().satisfies(exportedJsModule -> { assertThat(exportedJsModule.getFileName()).isEqualTo(resource.getResourceKey()); assertThat(exportedJsModule.getData()).isEqualTo(Base64.getEncoder().encodeToString(resourceData)); + assertThat(exportedJsModule.getEtag()).isEqualTo(resourceInfo.getEtag()); }); doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk()); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java index a7b184de56..17804cdd29 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java @@ -65,6 +65,10 @@ public class DashboardSyncServiceTest extends AbstractControllerTest { * * TODO: * update system widgets with the new structure + * + * TODO CONSIDER + * leave image/resource link as is. when importing - keep track of imported resources (if the resource key changed) (map of resourceKey -> importedResourceKey). + * then resolve and update resource links if resource key is new (idx added) * */ // FIXME: need to update resource key all the time???? because etag is changed. same for images.... 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 f27395c698..abc7fbfaf3 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbResource; @@ -47,6 +48,8 @@ public interface ImageService { byte[] getImagePreview(TenantId tenantId, TbResourceId imageId); + ResourceExportData exportImage(TbResourceInfo imageInfo); + TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force); String calculateImageEtag(byte[] imageData); 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 e8ae454ffc..3f62136ef4 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 @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.resource; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -42,6 +43,8 @@ public interface ResourceService extends EntityDaoService { byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); + ResourceExportData exportResource(TbResourceInfo resourceInfo); + TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java index 14d27bde6a..9d9cb538ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java @@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; @JsonInclude(JsonInclude.Include.NON_NULL) public class ResourceExportData { + private String etag; private String title; private ResourceType type; private ResourceSubType subType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java index 53e8ac10eb..d025aab3d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java @@ -15,11 +15,16 @@ */ package org.thingsboard.server.common.data.widget; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; +import java.util.Optional; + @Data public class WidgetType extends BaseWidgetType { @@ -43,4 +48,21 @@ public class WidgetType extends BaseWidgetType { this.descriptor = widgetType.getDescriptor(); } + @JsonIgnore + public JsonNode getDefaultConfig() { + return Optional.ofNullable(descriptor.get("defaultConfig")) + .filter(JsonNode::isTextual).map(JsonNode::asText) + .map(json -> { + try { + return mapper.readTree(json); + } catch (JsonProcessingException e) { + return null; + } + }).orElse(null); + } + + public void setDefaultConfig(JsonNode defaultConfig) { + ((ObjectNode) descriptor).put("defaultConfig", defaultConfig.toString()); + } + } 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 f69a564936..aef982f131 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 @@ -33,7 +33,7 @@ import java.util.List; @Data @EqualsAndHashCode(callSuper = true) -@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId"}) +@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity { @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index dd79bab265..0e9c6c8d48 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -469,6 +470,22 @@ public class JacksonUtil { } public static void replaceAllByMapping(JsonNode jsonNode, Map mapping, Map templateParams, BiFunction processor) { + replaceByMapping(jsonNode, mapping, templateParams, (name, value) -> { + if (value.isTextual()) { + return new TextNode(processor.apply(name, value.asText())); + } else if (value.isArray()) { + ArrayNode array = (ArrayNode) value; + for (int i = 0; i < array.size(); i++) { + String arrayElementName = name.replace("$index", Integer.toString(i)); + array.set(i, processor.apply(arrayElementName, array.get(i).asText())); + } + return array; + } + return value; + }); + } + + public static void replaceByMapping(JsonNode jsonNode, Map mapping, Map templateParams, BiFunction processor) { for (var entry : mapping.entrySet()) { String expression = entry.getValue(); Queue tasks = new LinkedList<>(); @@ -515,15 +532,7 @@ public class JacksonUtil { for (var replacement : task.getVariables().entrySet()) { name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); } - if (node.isObject() && value.isTextual()) { - ((ObjectNode) node).put(token, processor.apply(name, value.asText())); - } else if (value.isArray()) { - ArrayNode array = (ArrayNode) value; - for (int i = 0; i < array.size(); i++) { - String arrayElementName = name.replace("$index", Integer.toString(i)); - array.set(i, processor.apply(arrayElementName, array.get(i).asText())); - } - } + ((ObjectNode) node).set(token, processor.apply(name, value)); } else { if (StringUtils.isNotEmpty(variableName)) { tasks.add(task.next(value, variableName, variableValue)); 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 379fe615a5..44046a92ad 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 @@ -33,6 +33,7 @@ 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.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbImageDeleteResult; @@ -62,7 +63,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -75,7 +75,6 @@ import static org.apache.commons.lang3.ArrayUtils.get; public class BaseImageService extends BaseResourceService implements ImageService { private static final int MAX_ENTITIES_TO_FIND = 10; - private static final String DEFAULT_CONFIG_TAG = "defaultConfig"; public static Map DASHBOARD_BASE64_MAPPING = new HashMap<>(); public static Map WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); @@ -232,6 +231,24 @@ public class BaseImageService extends BaseResourceService implements ImageServic return resourceDao.getResourcePreview(tenantId, imageId); } + @Override + public ResourceExportData exportImage(TbResourceInfo imageInfo) { + ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class); + byte[] data = getImageData(imageInfo.getTenantId(), imageInfo.getId()); + return ResourceExportData.builder() + .mediaType(descriptor.getMediaType()) + .fileName(imageInfo.getFileName()) + .title(imageInfo.getTitle()) + .type(ResourceType.IMAGE) + .subType(imageInfo.getResourceSubType()) + .resourceKey(imageInfo.getResourceKey()) + .isPublic(imageInfo.isPublic()) + .publicResourceKey(imageInfo.getPublicResourceKey()) + .data(Base64.getEncoder().encodeToString(data)) + .etag(descriptor.getEtag()) + .build(); + } + @Override public TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force) { var tenantId = imageInfo.getTenantId(); @@ -299,13 +316,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic entity.setImage(result.getValue()); boolean updated = result.isUpdated(); if (entity.getDescriptor().isObject()) { - ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); - JsonNode defaultConfig = Optional.ofNullable(descriptor.get(DEFAULT_CONFIG_TAG)) - .filter(JsonNode::isTextual).map(JsonNode::asText) - .map(JacksonUtil::toJsonNode).orElse(null); + JsonNode defaultConfig = entity.getDefaultConfig(); if (defaultConfig != null) { updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); - descriptor.put(DEFAULT_CONFIG_TAG, defaultConfig.toString()); + entity.setDefaultConfig(defaultConfig); } } updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); @@ -341,7 +355,7 @@ 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:(.*);.*"); + public static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^;]+);data:(.*);.*"); private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) { if (StringUtils.isBlank(data)) { @@ -352,7 +366,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic String resourceName = null; String resourceSubType = null; String etag = null; - String mediaType; + String mediaType = null; var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data); if (matcher.matches()) { String[] metadata = matcher.group(1).split(":"); @@ -361,6 +375,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic resourceSubType = decode(get(metadata, 2)); etag = get(metadata, 3); mediaType = matcher.group(2); + } else if (data.startsWith("tb-image:")) { + etag = StringUtils.substringAfter(data, "tb-image:"); } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) { mediaType = StringUtils.substringBetween(data, "data:", ";base64"); } else { @@ -370,6 +386,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic String base64Data = StringUtils.substringAfter(data, "base64,"); byte[] imageData = StringUtils.isNotEmpty(base64Data) ? Base64.getDecoder().decode(base64Data) : null; if (StringUtils.isBlank(etag)) { + if (imageData == null) { + return UpdateResult.of(false, data); + } etag = calculateEtag(imageData); } var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag); @@ -454,14 +473,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic inlineImage(widgetTypeDetails, images); ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor(); inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor, images); - if (descriptor.has(DEFAULT_CONFIG_TAG) && descriptor.get(DEFAULT_CONFIG_TAG).isTextual()) { - try { - var defaultConfig = JacksonUtil.toJsonNode(descriptor.get(DEFAULT_CONFIG_TAG).asText()); - inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images); - descriptor.put(DEFAULT_CONFIG_TAG, JacksonUtil.toString(defaultConfig)); - } catch (Exception e) { - log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e); - } + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); + if (defaultConfig != null) { + inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images); + widgetTypeDetails.setDefaultConfig(defaultConfig); } return images; } @@ -501,28 +516,25 @@ public class BaseImageService extends BaseResourceService implements ImageServic private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix, List processedImages) { return inlineImage(tenantId, path, url, (key, imageInfo) -> { - String tbImagePrefix = ""; - boolean addData = true; + ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); + String value = ""; if (addTbImagePrefix) { - tbImagePrefix = "tb-image:" + encode(imageInfo.getResourceKey()) + ":" - + encode(imageInfo.getName()) + ":" - + encode(imageInfo.getResourceSubType().name()) + ":" - + imageInfo.getEtag() + ";"; + value = "tb-image:"; - if (processedImages != null && !key.isPreview()) { - addData = false; + if (processedImages != null && !key.isPreview()) { // images are stored separately processedImages.add(imageInfo); + value += descriptor.getEtag(); + return value; } - } - byte[] data; - if (addData) { - data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); - } else { - data = null; + value += encode(imageInfo.getResourceKey()) + ":" + + encode(imageInfo.getName()) + ":" + + encode(imageInfo.getResourceSubType().name()) + ":" + + imageInfo.getEtag() + ";"; } - ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview()); - return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + encode(data); + + byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); + return value + "data:" + descriptor.getMediaType() + ";base64," + encode(data); }); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 3ccab840ab..146be64a54 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.hash.Hashing; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; @@ -33,6 +34,7 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; 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.ResourceExportData; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -81,11 +83,13 @@ public class BaseResourceService extends AbstractCachedEntityService DASHBOARD_RESOURCES_MAPPING = Map.of( - "widgets.*.config.actions.*.*.customResources.*.url.id", "" + "widgets.*.config.actions.*.*.customResources.*.url", "" ); - private static final Map WIDGET_RESOURCES_MAPPING = Map.of( - "resources.*.url.id", "" + "resources.*.url", "" + ); + private static final Map WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING = Map.of( + "actions.*.*.customResources.*.url", "" ); @Override @@ -180,6 +184,21 @@ public class BaseResourceService extends AbstractCachedEntityService mapping) { @@ -314,7 +339,7 @@ public class BaseResourceService extends AbstractCachedEntityService replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails) { - return replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); + List resources = replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); + JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); + if (defaultConfig != null) { + resources.addAll(replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING)); + widgetTypeDetails.setDefaultConfig(defaultConfig); + } + return resources; } private List replaceResourcesUrlsWithTags(TenantId tenantId, JsonNode jsonNode, Map mapping) { @@ -377,7 +408,7 @@ public class BaseResourceService extends AbstractCachedEntityService mapping, UnaryOperator processor) { - JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { - if (StringUtils.isBlank(value)) { - return value; + JacksonUtil.replaceByMapping(jsonNode, mapping, Collections.emptyMap(), (name, urlNode) -> { + String value = null; + if (urlNode.isTextual()) { // link is in the right place + value = urlNode.asText(); + } else { + JsonNode id = urlNode.get("id"); // old structure is used + if (id != null && id.isTextual()) { + value = id.asText(); + } } - String newValue = processor.apply(value); - log.trace("Replaced '{}' with '{}'", value, newValue); + + if (StringUtils.isNotBlank(value)) { + value = processor.apply(value); + } else { + value = ""; + } + + JsonNode newValue = new TextNode(value); + log.trace("Replaced '{}' with '{}'", urlNode, newValue); return newValue; }); }