Browse Source

Reference images and resources only by link

pull/11873/head
ViacheslavKlimov 2 years ago
parent
commit
286363d985
  1. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
  3. 16
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  4. 7
      application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java
  5. 37
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java
  9. 28
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  10. 10
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
  11. 8
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  12. 10
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java
  13. 4
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  14. 217
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  15. 105
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  16. 4
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

2
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncService.java

@ -80,6 +80,8 @@ public class DashboardSyncService {
.map(widgetTypeFile -> getFileContent(widgetTypeFile.path()));
widgetsBundleService.updateSystemWidgets(widgetsBundles, widgetTypes);
// TODO: read images folder and save images
RepoFile dashboardFile = listFiles("dashboards").get(0);
String dashboardJson = getFileContent(dashboardFile.path());
resourceService.createOrUpdateSystemResource(ResourceType.DASHBOARD, GATEWAYS_DASHBOARD_KEY, dashboardJson);

2
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java

@ -51,7 +51,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
public Dashboard save(Dashboard dashboard, SecurityUser user) throws Exception {
ActionType actionType = dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = dashboard.getTenantId();
if (CollectionUtils.isNotEmpty(dashboard.getResources())) {
tbResourceService.importResources(dashboard.getResources(), user);
}

16
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -28,9 +28,13 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Customer;
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.TbResourceInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.page.PageDataIterable;
@ -41,6 +45,7 @@ import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@ -51,8 +56,13 @@ import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@ -90,6 +100,8 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private ResourceService resourceService;
@Autowired
private ImageService imageService;
@Autowired
private DashboardService dashboardService;
@ -367,7 +379,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
int updatedCount = 0;
for (DashboardId dashboardId : dashboards) {
Dashboard dashboard = dashboardService.findDashboardById(TenantId.SYS_TENANT_ID, dashboardId);
boolean updated = resourceService.replaceResourcesUsageWithUrls(dashboard);
boolean updated = resourceService.updateResourcesUsage(dashboard);
if (updated) {
dashboardService.saveDashboard(dashboard);
updatedCount++;
@ -386,7 +398,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
var widgets = new PageDataIterable<>(widgetTypeService::findAllWidgetTypesIds, 512);
for (WidgetTypeId widgetTypeId : widgets) {
WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId);
boolean updated = resourceService.replaceResourcesUsageWithUrls(widgetTypeDetails);
boolean updated = resourceService.updateResourcesUsage(widgetTypeDetails);
if (updated) {
widgetTypeService.saveWidgetType(widgetTypeDetails);
updatedCount++;

7
application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java

@ -59,17 +59,18 @@ public class ImagesUpdater {
public void updateWidgetTypesImages() {
log.info("Updating widget types images...");
var widgetTypesIds = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024);
updateImages(widgetTypesIds, "widget type", imageService::replaceBase64WithImageUrl, widgetTypeDao);
updateImages(widgetTypesIds, "widget type", imageService::updateImagesUsage, widgetTypeDao);
}
public void updateDashboardsImages() {
log.info("Updating dashboards images...");
updateImages("dashboard", dashboardDao::findIdsByTenantId, imageService::replaceBase64WithImageUrl, dashboardDao);
updateImages("dashboard", dashboardDao::findIdsByTenantId, imageService::updateImagesUsage, dashboardDao);
}
public void createSystemImages(Dashboard defaultDashboard) {
defaultDashboard.setTenantId(TenantId.SYS_TENANT_ID);
boolean created = imageService.replaceBase64WithImageUrl(defaultDashboard);
// TODO: test! also update dashboards
boolean created = imageService.updateImagesUsage(defaultDashboard);
if (created) {
log.debug("Created system images for default dashboard '{}'", defaultDashboard.getTitle());
}

37
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -43,11 +43,11 @@ import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -135,41 +135,40 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
@Override
public List<ResourceExportData> exportResources(Dashboard dashboard, SecurityUser user) throws ThingsboardException {
return exportResources(dashboard, imageService::inlineImages, resourceService::replaceResourcesUrlsWithTags, user);
return exportResources(dashboard, imageService::getUsedImages, resourceService::getUsedResources, user);
}
@Override
public List<ResourceExportData> exportResources(WidgetTypeDetails widgetTypeDetails, SecurityUser user) throws ThingsboardException {
return exportResources(widgetTypeDetails, imageService::inlineImages, resourceService::replaceResourcesUrlsWithTags, user);
return exportResources(widgetTypeDetails, imageService::getUsedImages, resourceService::getUsedResources, user);
}
@Override
public void importResources(List<ResourceExportData> resources, SecurityUser user) throws Exception {
for (ResourceExportData resourceExportData : resources) {
if (resourceExportData.getType() == ResourceType.IMAGE) {
tbImageService.importImage(resourceExportData, true, user);
for (ResourceExportData resourceData : resources) {
TbResourceInfo resourceInfo;
if (resourceData.getType() == ResourceType.IMAGE) {
resourceInfo = tbImageService.importImage(resourceData, true, user);
} else {
importResource(resourceExportData, true, user);
resourceInfo = importResource(resourceData, true, user);
}
resourceData.setNewLink(resourceInfo.getLink());
}
}
private <T> List<ResourceExportData> exportResources(T entity,
Function<T, List<TbResourceInfo>> imagesProcessor,
Function<T, List<TbResourceInfo>> resourcesProcessor,
Function<T, Collection<TbResourceInfo>> imagesProcessor,
Function<T, Collection<TbResourceInfo>> resourcesProcessor,
SecurityUser user) throws ThingsboardException {
Map<TbResourceId, TbResourceInfo> resources = new HashMap<>();
for (TbResourceInfo imageInfo : imagesProcessor.apply(entity)) {
resources.putIfAbsent(imageInfo.getId(), imageInfo);
}
for (TbResourceInfo resourceInfo : resourcesProcessor.apply(entity)) {
resources.putIfAbsent(resourceInfo.getId(), resourceInfo);
}
for (TbResourceInfo resourceInfo : resources.values()) {
List<TbResourceInfo> resources = new ArrayList<>();
resources.addAll(imagesProcessor.apply(entity));
resources.addAll(resourcesProcessor.apply(entity));
for (TbResourceInfo resourceInfo : resources) {
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.READ, resourceInfo.getId(), resourceInfo);
}
return resources.values().stream()
return resources.stream()
.map(resourceInfo -> {
if (resourceInfo.getResourceType() == ResourceType.IMAGE) {
ResourceExportData imageExportData = imageService.exportImage(resourceInfo);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java

@ -48,8 +48,6 @@ public class DashboardExportService extends BaseEntityExportService<DashboardId,
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.emptySet(), WIDGET_CONFIG_PROCESSED_FIELDS_PATTERN);
}
imageService.inlineImages(dashboard);
resourceService.replaceResourcesUrlsWithTags(dashboard);
}
@Override

3
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java

@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
@ -63,8 +62,6 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
private AttributesService attributesService;
@Autowired
protected ImageService imageService;
@Autowired
protected ResourceService resourceService;
@Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetTypeExportService.java

@ -36,8 +36,6 @@ public class WidgetTypeExportService extends BaseEntityExportService<WidgetTypeI
if (widgetTypeDetails.getTenantId() == null || widgetTypeDetails.getTenantId().isNullUid()) {
throw new IllegalArgumentException("Export of system Widget Type is not allowed");
}
imageService.inlineImages(widgetTypeDetails);
resourceService.replaceResourcesUrlsWithTags(widgetTypeDetails);
}
@Override

28
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java

@ -587,7 +587,7 @@ public class DashboardControllerTest extends AbstractControllerTest {
@Test
public void testExportImportDashboardWithResources() throws Exception {
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", "image12.png", "image/png", ImageControllerTest.PNG_IMAGE);
TbResourceInfo imageInfo = uploadImage(HttpMethod.POST, "/api/image", "image12", "image/png", ImageControllerTest.PNG_IMAGE);
TbResource resource = new TbResource();
resource.setResourceKey("gateway-management-extension.js");
resource.setFileName(resource.getResourceKey());
@ -613,42 +613,42 @@ public class DashboardControllerTest extends AbstractControllerTest {
Dashboard exportedDashboard = doGet("/api/dashboard/" + dashboard.getUuidId() + "?includeResources=true", Dashboard.class);
exportedDashboard.setId(null);
String imageRef = exportedDashboard.getConfiguration().get("someImage").asText();
assertThat(imageRef).isEqualTo("tb-image:" + Base64.getEncoder().encodeToString(imageInfo.getResourceKey().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getName().getBytes()) + ":"
+ Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":"
+ imageInfo.getEtag() + ";data:image/png;base64,");
assertThat(imageRef).isEqualTo("tb-image;/api/images/tenant/image12");
String resourceRef = exportedDashboard.getConfiguration().get("widgets").get("xxx").get("config")
.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());
assertThat(resourceRef).isEqualTo("tb-resource;/api/resource/js_module/tenant/gateway-management-extension.js");
Map<ResourceType, List<ResourceExportData>> resources = exportedDashboard.getResources().stream()
.collect(Collectors.groupingBy(ResourceExportData::getType));
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.getFileName()).isEqualTo(resourceInfo.getResourceKey());
assertThat(exportedJsModule.getData()).isEqualTo(Base64.getEncoder().encodeToString(resourceData));
assertThat(exportedJsModule.getEtag()).isEqualTo(resourceInfo.getEtag());
});
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());
doDelete("/api/images/tenant/" + imageInfo.getResourceKey()).andExpect(status().isOk());
doDelete("/api/resource/" + resourceInfo.getId()).andExpect(status().isOk());
resource = new TbResource(resourceInfo);
resource.setData(new byte[]{1, 2, 3}); // updating resource data to check that a new resource will be created
doPost("/api/resource", resource, TbResourceInfo.class);
Dashboard importedDashboard = doPost("/api/dashboard", exportedDashboard, Dashboard.class);
assertThat(importedDashboard.getConfiguration().get("someImage").asText()).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey());
imageRef = importedDashboard.getConfiguration().get("someImage").asText();
assertThat(imageRef).isEqualTo("tb-image;/api/images/tenant/" + imageInfo.getResourceKey());
resourceRef = importedDashboard.getConfiguration().get("widgets").get("xxx").get("config")
.get("actions").get("elementClick").get(0).get("customResources").get(0).get("url").asText();
String newResourceKey = "gateway-management-extension_(1).js";
assertThat(resourceRef).isEqualTo("tb-resource;/api/resource/js_module/tenant/" + newResourceKey);
TbResourceInfo importedImageInfo = doGet("/api/images/tenant/" + imageInfo.getResourceKey() + "/info", TbResourceInfo.class);
assertThat(importedImageInfo.getEtag()).isEqualTo(imageInfo.getEtag());
assertThat(importedImageInfo.getResourceKey()).isEqualTo(imageInfo.getResourceKey());
TbResourceInfo importedResourceInfo = doGet(resourceInfo.getLink() + "/info", TbResourceInfo.class);
TbResourceInfo importedResourceInfo = doGet("/api/resource/js_module/tenant/" + newResourceKey + "/info", TbResourceInfo.class);
assertThat(importedResourceInfo.getEtag()).isEqualTo(resourceInfo.getEtag());
assertThat(importedResourceInfo.getResourceKey()).isEqualTo(resourceInfo.getResourceKey());
}
private Dashboard createDashboard(String title) {

10
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java

@ -28,7 +28,7 @@ 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 java.util.List;
import java.util.Collection;
public interface ImageService {
@ -58,15 +58,15 @@ public interface ImageService {
boolean replaceBase64WithImageUrl(HasImage entity, String type);
boolean replaceBase64WithImageUrl(Dashboard dashboard);
boolean updateImagesUsage(Dashboard dashboard);
boolean replaceBase64WithImageUrl(WidgetTypeDetails widgetType);
boolean updateImagesUsage(WidgetTypeDetails widgetType);
void inlineImage(HasImage entity);
List<TbResourceInfo> inlineImages(Dashboard dashboard);
Collection<TbResourceInfo> getUsedImages(Dashboard dashboard);
List<TbResourceInfo> inlineImages(WidgetTypeDetails widgetTypeDetails);
Collection<TbResourceInfo> getUsedImages(WidgetTypeDetails widgetTypeDetails);
void inlineImageForEdge(HasImage entity);

8
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -73,13 +73,13 @@ public interface ResourceService extends EntityDaoService {
TbResourceInfo findSystemOrTenantResourceByEtag(TenantId tenantId, ResourceType resourceType, String etag);
boolean replaceResourcesUsageWithUrls(Dashboard dashboard);
boolean updateResourcesUsage(Dashboard dashboard);
boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails);
boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails);
List<TbResourceInfo> replaceResourcesUrlsWithTags(Dashboard dashboard);
List<TbResourceInfo> getUsedResources(Dashboard dashboard);
List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails);
List<TbResourceInfo> getUsedResources(WidgetTypeDetails widgetTypeDetails);
TbResource createOrUpdateSystemResource(ResourceType resourceType, String resourceKey, String data);

10
common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@ -32,7 +33,7 @@ import lombok.extern.slf4j.Slf4j;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResourceExportData {
private String etag;
private String link;
private String title;
private ResourceType type;
private ResourceSubType subType;
@ -43,4 +44,11 @@ public class ResourceExportData {
private String mediaType;
private String data;
/*
* when importing resource, the previous link may be changed due to existing duplicates or something else.
* this is the new proper link to be used in place of the old link
* */
@JsonIgnore
private String newLink;
}

4
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -161,8 +161,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
dashboardValidator.validate(dashboard, DashboardInfo::getTenantId);
}
try {
imageService.replaceBase64WithImageUrl(dashboard);
resourceService.replaceResourcesUsageWithUrls(dashboard);
imageService.updateImagesUsage(dashboard);
resourceService.updateResourcesUsage(dashboard);
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));

217
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.SneakyThrows;
@ -57,8 +56,8 @@ import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -236,6 +235,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
byte[] data = getImageData(imageInfo.getTenantId(), imageInfo.getId());
return ResourceExportData.builder()
.link(imageInfo.getLink())
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
@ -245,7 +245,6 @@ public class BaseImageService extends BaseResourceService implements ImageServic
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
.data(Base64.getEncoder().encodeToString(data))
.etag(descriptor.getEtag())
.build();
}
@ -298,51 +297,59 @@ public class BaseImageService extends BaseResourceService implements ImageServic
}
imageName = imageName + type + " image";
UpdateResult result = base64ToImageUrl(entity.getTenantId(), imageName, entity.getImage());
UpdateResult result = convertToImageUrl(entity.getTenantId(), imageName, entity.getImage(), Collections.emptyMap());
entity.setImage(result.getValue());
return result.isUpdated();
}
@Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@Override
public boolean replaceBase64WithImageUrl(WidgetTypeDetails entity) {
log.trace("Executing replaceBase64WithImageUrl [{}] [WidgetTypeDetails] [{}]", entity.getTenantId(), entity.getId());
String prefix = "\"" + entity.getName() + "\" ";
if (entity.getTenantId() == null || entity.getTenantId().isSysTenantId()) {
public boolean updateImagesUsage(WidgetTypeDetails widgetTypeDetails) {
TenantId tenantId = widgetTypeDetails.getTenantId();
log.trace("Executing updateImagesUsage [{}] [WidgetTypeDetails] [{}]", tenantId, widgetTypeDetails.getId());
String prefix = "\"" + widgetTypeDetails.getName() + "\" ";
if (tenantId == null || tenantId.isSysTenantId()) {
prefix += "system ";
}
prefix += "widget";
UpdateResult result = base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage());
entity.setImage(result.getValue());
Map<String, String> imagesLinks = getResourcesLinks(widgetTypeDetails.getResources());
UpdateResult result = convertToImageUrl(tenantId, prefix + " image", widgetTypeDetails.getImage(), imagesLinks);
boolean updated = result.isUpdated();
if (entity.getDescriptor().isObject()) {
JsonNode defaultConfig = entity.getDefaultConfig();
widgetTypeDetails.setImage(result.getValue());
if (widgetTypeDetails.getDescriptor().isObject()) {
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
if (defaultConfig != null) {
updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig);
entity.setDefaultConfig(defaultConfig);
updated |= convertToImageUrlsByMapping(tenantId, WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig, imagesLinks);
widgetTypeDetails.setDefaultConfig(defaultConfig);
}
}
updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor());
updated |= convertToImageUrls(tenantId, prefix, widgetTypeDetails.getDescriptor(), imagesLinks);
return updated;
}
@Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure
@Override
public boolean replaceBase64WithImageUrl(Dashboard entity) {
log.trace("Executing replaceBase64WithImageUrl [{}] [Dashboard] [{}]", entity.getTenantId(), entity.getId());
String prefix = "\"" + entity.getTitle() + "\" dashboard";
var result = base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage());
public boolean updateImagesUsage(Dashboard dashboard) {
TenantId tenantId = dashboard.getTenantId();
log.trace("Executing updateImagesUsage [{}] [Dashboard] [{}]", tenantId, dashboard.getId());
String prefix = "\"" + dashboard.getTitle() + "\" dashboard";
Map<String, String> imagesLinks = getResourcesLinks(dashboard.getResources());
var result = convertToImageUrl(tenantId, prefix + " image", dashboard.getImage(), imagesLinks);
boolean updated = result.isUpdated();
entity.setImage(result.getValue());
updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), entity.getConfiguration());
updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getConfiguration());
dashboard.setImage(result.getValue());
updated |= convertToImageUrlsByMapping(tenantId, DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), dashboard.getConfiguration(), imagesLinks);
updated |= convertToImageUrls(tenantId, prefix, dashboard.getConfiguration(), imagesLinks);
return updated;
}
private boolean base64ToImageUrlUsingMapping(TenantId tenantId, Map<String, String> mapping, Map<String, String> templateParams, JsonNode configuration) {
private boolean convertToImageUrlsByMapping(TenantId tenantId, Map<String, String> mapping, Map<String, String> templateParams, JsonNode configuration, Map<String, String> links) {
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> {
UpdateResult result = base64ToImageUrl(tenantId, name, value);
UpdateResult result = convertToImageUrl(tenantId, name, value, links);
if (result.isUpdated()) {
updated.set(true);
}
@ -351,22 +358,32 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return updated.get();
}
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data) {
return base64ToImageUrl(tenantId, name, data, false);
private UpdateResult convertToImageUrl(TenantId tenantId, String name, String data, Map<String, String> links) {
return convertToImageUrl(tenantId, name, data, false, links);
}
public static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:([^;]+);data:(.*);.*");
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) {
private UpdateResult convertToImageUrl(TenantId tenantId, String name, String data, boolean strict, Map<String, String> imagesLinks) {
if (StringUtils.isBlank(data)) {
return UpdateResult.of(false, data);
}
String link = getImageLink(data);
if (link != null) {
String newLink = imagesLinks.get(link);
if (newLink == null || newLink.equals(link)) {
return UpdateResult.of(false, data);
} else {
return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + newLink);
}
}
String resourceKey = null;
String resourceName = null;
String resourceSubType = null;
String etag = null;
String mediaType = null;
String mediaType;
var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data);
if (matcher.matches()) {
String[] metadata = matcher.group(1).split(":");
@ -375,8 +392,6 @@ 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 {
@ -386,9 +401,6 @@ 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);
@ -439,10 +451,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink());
}
private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) {
private boolean convertToImageUrls(TenantId tenantId, String title, JsonNode root, Map<String, String> links) {
AtomicBoolean updated = new AtomicBoolean(false);
JacksonUtil.replaceAll(root, title, (path, value) -> {
UpdateResult result = base64ToImageUrl(tenantId, path, value, true);
UpdateResult result = convertToImageUrl(tenantId, path, value, true, links);
if (result.isUpdated()) {
updated.set(true);
}
@ -454,101 +466,116 @@ public class BaseImageService extends BaseResourceService implements ImageServic
@Override
public void inlineImage(HasImage entity) {
log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
inlineImage(entity, null);
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), true));
}
@Override
public List<TbResourceInfo> inlineImages(Dashboard dashboard) {
log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
List<TbResourceInfo> images = new ArrayList<>();
inlineImage(dashboard, images);
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), images);
return images;
public Collection<TbResourceInfo> getUsedImages(Dashboard dashboard) {
TenantId tenantId = dashboard.getTenantId();
log.trace("Executing getUsedImages [{}] [Dashboard] [{}]", tenantId, dashboard.getId());
Map<TbResourceId, TbResourceInfo> images = new HashMap<>();
processImage(tenantId, "image", dashboard.getImage(), (key, imageInfo) -> {
images.putIfAbsent(imageInfo.getId(), imageInfo);
return null; // leaving the url as is
});
processImages(tenantId, dashboard.getConfiguration(), (key, imageInfo) -> {
images.putIfAbsent(imageInfo.getId(), imageInfo);
return null; // leaving the url as is
});
return images.values();
}
@Override
public List<TbResourceInfo> inlineImages(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
List<TbResourceInfo> images = new ArrayList<>();
inlineImage(widgetTypeDetails, images);
ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor();
inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor, images);
public Collection<TbResourceInfo> getUsedImages(WidgetTypeDetails widgetTypeDetails) {
TenantId tenantId = widgetTypeDetails.getTenantId();
log.trace("Executing getUsedImages [{}] [WidgetTypeDetails] [{}]", tenantId, widgetTypeDetails.getId());
Map<TbResourceId, TbResourceInfo> images = new HashMap<>();
processImage(tenantId, "image", widgetTypeDetails.getImage(), (key, imageInfo) -> {
images.putIfAbsent(imageInfo.getId(), imageInfo);
return null; // leaving the url as is
});
processImages(tenantId, widgetTypeDetails.getDescriptor(), (key, imageInfo) -> {
images.putIfAbsent(imageInfo.getId(), imageInfo);
return null; // leaving the url as is
});
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
if (defaultConfig != null) {
inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images);
widgetTypeDetails.setDefaultConfig(defaultConfig);
processImages(tenantId, defaultConfig, (key, imageInfo) -> {
images.putIfAbsent(imageInfo.getId(), imageInfo);
return null; // leaving the url as is
});
}
return images;
return images.values();
}
@Override
public void inlineImageForEdge(HasImage entity) {
log.trace("Executing inlineImageForEdge [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), false, null));
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), false));
}
@Override
public void inlineImagesForEdge(Dashboard dashboard) {
log.trace("Executing inlineImagesForEdge [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
inlineImageForEdge(dashboard);
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration(), false, null);
inlineImages(dashboard.getTenantId(), dashboard.getConfiguration(), false);
}
@Override
public void inlineImagesForEdge(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
inlineImageForEdge(widgetTypeDetails);
inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false, null);
inlineImages(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), false);
}
private void inlineImage(HasImage entity, List<TbResourceInfo> processedImages) {
log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage(), true, processedImages));
}
private void inlineIntoJson(TenantId tenantId, JsonNode root, List<TbResourceInfo> processedImages) {
inlineIntoJson(tenantId, root, true, processedImages);
private void inlineImages(TenantId tenantId, JsonNode root, boolean addTbImagePrefix) {
processImages(tenantId, root, (key, imageInfo) -> {
return inlineImage(key, imageInfo, addTbImagePrefix);
});
}
private void inlineIntoJson(TenantId tenantId, JsonNode root, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
JacksonUtil.replaceAll(root, "", (path, value) -> inlineImage(tenantId, path, value, addTbImagePrefix, processedImages));
private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix) {
return processImage(tenantId, path, url, (key, imageInfo) -> {
return inlineImage(key, imageInfo, addTbImagePrefix);
});
}
private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
return inlineImage(tenantId, path, url, (key, imageInfo) -> {
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
String value = "";
if (addTbImagePrefix) {
value = "tb-image:";
if (processedImages != null && !key.isPreview()) { // images are stored separately
processedImages.add(imageInfo);
value += descriptor.getEtag();
return value;
}
private String inlineImage(ImageCacheKey key, TbResourceInfo imageInfo, boolean addTbImagePrefix) {
String value = "";
if (addTbImagePrefix) {
value = "tb-image:" + encode(imageInfo.getResourceKey()) + ":"
+ encode(imageInfo.getName()) + ":"
+ encode(imageInfo.getResourceSubType().name()) + ";";
}
value += encode(imageInfo.getResourceKey()) + ":"
+ encode(imageInfo.getName()) + ":"
+ encode(imageInfo.getResourceSubType().name()) + ":"
+ imageInfo.getEtag() + ";";
}
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
byte[] data = key.isPreview() ? getImagePreview(key.getTenantId(), imageInfo.getId()) : getImageData(key.getTenantId(), imageInfo.getId());
return value + "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);
private void processImages(TenantId tenantId, JsonNode node, BiFunction<ImageCacheKey, TbResourceInfo, String> processor) {
JacksonUtil.replaceAll(node, "", (path, value) -> {
return processImage(tenantId, path, value, processor);
});
}
private String inlineImage(TenantId tenantId, String path, String imageUrl, BiFunction<ImageCacheKey, TbResourceInfo, String> inliner) {
private String processImage(TenantId tenantId, String path, String imageUrl, BiFunction<ImageCacheKey, TbResourceInfo, String> processor) {
try {
ImageCacheKey key = getKeyFromUrl(tenantId, imageUrl);
if (key != null) {
var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey());
if (imageInfo != null && !(TenantId.SYS_TENANT_ID.equals(imageInfo.getTenantId()) && ResourceSubType.SCADA_SYMBOL.equals(imageInfo.getResourceSubType()))) {
return inliner.apply(key, imageInfo);
// TODO: maybe export scada too?
if (imageInfo == null || (TenantId.SYS_TENANT_ID.equals(imageInfo.getTenantId()) && ResourceSubType.SCADA_SYMBOL.equals(imageInfo.getResourceSubType()))) {
return imageUrl;
} else {
String result = processor.apply(key, imageInfo);
if (result != null) {
return result;
}
}
}
} catch (Exception e) {
log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, imageUrl, e);
log.warn("[{}][{}][{}] Failed to process image", tenantId, path, imageUrl, e);
}
return imageUrl;
}
@ -562,10 +589,15 @@ public class BaseImageService extends BaseResourceService implements ImageServic
if (StringUtils.isBlank(url)) {
return null;
}
String link = getImageLink(url);
if (link == null) {
return null;
}
TenantId imageTenantId = null;
if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) {
if (link.startsWith("/api/images/tenant/")) {
imageTenantId = tenantId;
} else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) {
} else if (link.startsWith("/api/images/system/")) {
imageTenantId = TenantId.SYS_TENANT_ID;
}
if (imageTenantId != null) {
@ -579,10 +611,19 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return null;
}
private String getImageLink(String value) {
if (value.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images")) {
return StringUtils.removeStart(value, DataConstants.TB_IMAGE_PREFIX);
} else {
return null;
}
}
@Data(staticConstructor = "of")
private static class UpdateResult {
private final boolean updated;
private final String value;
}
}

105
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -21,6 +21,7 @@ import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
@ -58,6 +59,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -188,6 +190,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public ResourceExportData exportResource(TbResourceInfo resourceInfo) {
byte[] data = getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
return ResourceExportData.builder()
.link(resourceInfo.getLink())
.mediaType(resourceInfo.getResourceType().getMediaType())
.fileName(resourceInfo.getFileName())
.title(resourceInfo.getTitle())
@ -195,7 +198,6 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
.subType(resourceInfo.getResourceSubType())
.resourceKey(resourceInfo.getResourceKey())
.data(Base64.getEncoder().encodeToString(data))
.etag(resourceInfo.getEtag())
.build();
}
@ -311,41 +313,49 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
}
@Override
public boolean replaceResourcesUsageWithUrls(Dashboard dashboard) {
return replaceResourcesUsageWithUrls(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING);
public boolean updateResourcesUsage(Dashboard dashboard) {
Map<String, String> links = getResourcesLinks(dashboard.getResources());
return updateResourcesUsage(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING, links);
}
@Override
public boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails) {
boolean updated = replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING);
public boolean updateResourcesUsage(WidgetTypeDetails widgetTypeDetails) {
Map<String, String> links = getResourcesLinks(widgetTypeDetails.getResources());
boolean updated = updateResourcesUsage(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING, links);
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
if (defaultConfig != null) {
updated |= replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING);
updated |= updateResourcesUsage(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING, links);
widgetTypeDetails.setDefaultConfig(defaultConfig);
}
return updated;
}
private boolean replaceResourcesUsageWithUrls(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
AtomicBoolean updated = new AtomicBoolean(false);
processResources(jsonNode, mapping, value -> {
if (value.startsWith(DataConstants.TB_RESOURCE_PREFIX + "/api/resource")) { // already a link, ignoring
return value;
}
TbResourceInfo resourceInfo;
if (value.startsWith("tb-resource:")) { // tag with metadata, probably importing
String[] metadata = StringUtils.removeStart(value, "tb-resource:").split(":");
if (metadata.length < 2) {
return value;
protected Map<String, String> getResourcesLinks(List<ResourceExportData> resources) {
Map<String, String> links;
if (CollectionUtils.isNotEmpty(resources)) {
links = new HashMap<>();
resources.forEach(resource -> {
if (resource.getNewLink() != null) {
links.put(resource.getLink(), resource.getNewLink());
}
ResourceType resourceType = ResourceType.valueOf(metadata[0]);
String etag = metadata[1];
});
} else {
links = Collections.emptyMap();
}
return links;
}
resourceInfo = findSystemOrTenantResourceByEtag(tenantId, resourceType, etag);
if (resourceInfo == null) {
log.warn("[{}] Couldn't find resource referenced as '{}'", tenantId, value);
return value;
private boolean updateResourcesUsage(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping, Map<String, String> links) {
AtomicBoolean updated = new AtomicBoolean(false);
processResources(jsonNode, mapping, value -> {
String link = getResourceLink(value);
if (link != null) {
String newLink = links.get(link);
if (newLink == null || newLink.equals(link)) {
return value; // leaving link as is
} else {
updated.set(true);
return DataConstants.TB_RESOURCE_PREFIX + newLink;
}
} else { // probably importing an old dashboard json where resources are referenced by ids
TbResourceId resourceId;
@ -354,40 +364,39 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
} catch (IllegalArgumentException e) {
return value;
}
resourceInfo = findResourceInfoById(tenantId, resourceId);
if (resourceInfo == null) {
updated.set(true);
TbResourceInfo resourceInfo = findResourceInfoById(tenantId, resourceId);
updated.set(true);
if (resourceInfo != null) {
return DataConstants.TB_RESOURCE_PREFIX + resourceInfo.getLink();
} else {
log.warn("[{}] Couldn't find resource referenced as '{}'", tenantId, value);
return "";
}
}
updated.set(true);
return DataConstants.TB_RESOURCE_PREFIX + resourceInfo.getLink();
});
return updated.get();
}
@Override
public List<TbResourceInfo> replaceResourcesUrlsWithTags(Dashboard dashboard) {
return replaceResourcesUrlsWithTags(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING);
public List<TbResourceInfo> getUsedResources(Dashboard dashboard) {
return getUsedResources(dashboard.getTenantId(), dashboard.getConfiguration(), DASHBOARD_RESOURCES_MAPPING);
}
@Override
public List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails) {
List<TbResourceInfo> resources = replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING);
public List<TbResourceInfo> getUsedResources(WidgetTypeDetails widgetTypeDetails) {
List<TbResourceInfo> resources = getUsedResources(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);
resources.addAll(getUsedResources(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING));
}
return resources;
}
private List<TbResourceInfo> replaceResourcesUrlsWithTags(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
private List<TbResourceInfo> getUsedResources(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
List<TbResourceInfo> resources = new ArrayList<>();
processResources(jsonNode, mapping, value -> {
if (!value.startsWith(DataConstants.TB_RESOURCE_PREFIX + "/api/resource/")) {
String link = getResourceLink(value);
if (link == null) {
return value;
}
@ -395,27 +404,35 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
String resourceKey;
TenantId resourceTenantId;
try {
String[] parts = StringUtils.removeStart(value, DataConstants.TB_RESOURCE_PREFIX + "/api/resource/").split("/");
String[] parts = StringUtils.removeStart(link, "/api/resource/").split("/");
resourceType = ResourceType.valueOf(parts[0].toUpperCase());
String scope = parts[1];
resourceKey = parts[2];
resourceTenantId = scope.equals("system") ? TenantId.SYS_TENANT_ID : tenantId;
} catch (Exception e) {
log.warn("[{}] Invalid resource link '{}'", tenantResourcesRemover, value);
log.warn("[{}] Invalid resource link '{}'", tenantId, value);
return value;
}
TbResourceInfo resourceInfo = findResourceInfoByTenantIdAndKey(resourceTenantId, resourceType, resourceKey);
if (resourceInfo != null) {
resources.add(resourceInfo);
return "tb-resource:" + String.join(":", resourceType.name(), resourceInfo.getEtag());
} else {
return value;
log.warn("[{}] Unknown resource referenced with '{}'", tenantId, value);
}
return value;
});
return resources;
}
private String getResourceLink(String value) {
if (StringUtils.startsWith(value, DataConstants.TB_RESOURCE_PREFIX + "/api/resource/")) {
return StringUtils.removeStart(value, DataConstants.TB_RESOURCE_PREFIX);
} else {
return null;
}
}
private void processResources(JsonNode jsonNode, Map<String, String> mapping, UnaryOperator<String> processor) {
JacksonUtil.replaceByMapping(jsonNode, mapping, Collections.emptyMap(), (name, urlNode) -> {
String value = null;
@ -446,8 +463,8 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
Dashboard dashboard = JacksonUtil.fromString(data, Dashboard.class);
dashboard.setTenantId(TenantId.SYS_TENANT_ID);
imageService.replaceBase64WithImageUrl(dashboard);
replaceResourcesUsageWithUrls(dashboard);
imageService.updateImagesUsage(dashboard);
updateResourcesUsage(dashboard);
data = JacksonUtil.toString(dashboard);
}

4
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -98,8 +98,8 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
log.trace("Executing saveWidgetType [{}]", widgetTypeDetails);
widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId);
try {
imageService.replaceBase64WithImageUrl(widgetTypeDetails);
resourceService.replaceResourcesUsageWithUrls(widgetTypeDetails);
imageService.updateImagesUsage(widgetTypeDetails);
resourceService.updateResourcesUsage(widgetTypeDetails);
WidgetTypeDetails result = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(result.getTenantId())

Loading…
Cancel
Save