Browse Source

Move resource url from id to url; add etag to export data

pull/11873/head
ViacheslavKlimov 2 years ago
parent
commit
155b22accf
  1. 2
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  2. 17
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java
  3. 17
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java
  5. 4
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  6. 4
      application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java
  7. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
  8. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  9. 1
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceExportData.java
  10. 22
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
  11. 2
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  12. 27
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  13. 78
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  14. 68
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

2
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) @Parameter(description = IMAGE_KEY_PARAM_DESCRIPTION, required = true)
@PathVariable String key) throws Exception { @PathVariable String key) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ); TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
return tbImageService.exportImage(imageInfo); return imageService.exportImage(imageInfo);
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")

17
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 @Override
public TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception { public TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception {
TbResource image = new TbResource(); TbResource image = new TbResource();

17
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() return resources.values().stream()
.map(resourceInfo -> { .map(resourceInfo -> {
if (resourceInfo.getResourceType() == ResourceType.IMAGE) { 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 imageExportData.setResourceKey(null); // so that the image is not updated by resource key on import
return imageExportData; return imageExportData;
} else { } else {
return exportResource(resourceInfo); return resourceService.exportResource(resourceInfo);
} }
}) })
.toList(); .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 { private TbResourceInfo importResource(ResourceExportData exportData, boolean checkExisting, SecurityUser user) throws ThingsboardException {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) { || exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {

2
application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java

@ -37,8 +37,6 @@ public interface TbImageService {
void evictETags(ImageCacheKey imageCacheKey); void evictETags(ImageCacheKey imageCacheKey);
ResourceExportData exportImage(TbResourceInfo imageInfo);
TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception; TbResourceInfo importImage(ResourceExportData imageData, boolean checkExisting, SecurityUser user) throws Exception;
} }

4
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()) + ":" + Base64.getEncoder().encodeToString(imageInfo.getResourceSubType().name().getBytes()) + ":"
+ imageInfo.getEtag() + ";data:image/png;base64,"); + imageInfo.getEtag() + ";data:image/png;base64,");
String resourceRef = exportedDashboard.getConfiguration().get("widgets").get("xxx").get("config") 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()) assertThat(resourceRef).isEqualTo("tb-resource:" + Base64.getEncoder().encodeToString(resourceInfo.getResourceType().name().getBytes())
+ ":" + resourceInfo.getEtag()); + ":" + resourceInfo.getEtag());
@ -627,10 +627,12 @@ public class DashboardControllerTest extends AbstractControllerTest {
assertThat(resources.get(ResourceType.IMAGE)).singleElement().satisfies(exportedImage -> { assertThat(resources.get(ResourceType.IMAGE)).singleElement().satisfies(exportedImage -> {
assertThat(exportedImage.getFileName()).isEqualTo(imageInfo.getResourceKey()); assertThat(exportedImage.getFileName()).isEqualTo(imageInfo.getResourceKey());
assertThat(exportedImage.getData()).isEqualTo(Base64.getEncoder().encodeToString(ImageControllerTest.PNG_IMAGE)); 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(resources.get(ResourceType.JS_MODULE)).singleElement().satisfies(exportedJsModule -> {
assertThat(exportedJsModule.getFileName()).isEqualTo(resource.getResourceKey()); assertThat(exportedJsModule.getFileName()).isEqualTo(resource.getResourceKey());
assertThat(exportedJsModule.getData()).isEqualTo(Base64.getEncoder().encodeToString(resourceData)); assertThat(exportedJsModule.getData()).isEqualTo(Base64.getEncoder().encodeToString(resourceData));
assertThat(exportedJsModule.getEtag()).isEqualTo(resourceInfo.getEtag());
}); });
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk()); doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());

4
application/src/test/java/org/thingsboard/server/service/entitiy/dashboard/DashboardSyncServiceTest.java

@ -65,6 +65,10 @@ public class DashboardSyncServiceTest extends AbstractControllerTest {
* *
* TODO: * TODO:
* update system widgets with the new structure * 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.... // FIXME: need to update resource key all the time???? because etag is changed. same for images....

3
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.Dashboard;
import org.thingsboard.server.common.data.HasImage; 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.ResourceSubType;
import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
@ -47,6 +48,8 @@ public interface ImageService {
byte[] getImagePreview(TenantId tenantId, TbResourceId imageId); byte[] getImagePreview(TenantId tenantId, TbResourceId imageId);
ResourceExportData exportImage(TbResourceInfo imageInfo);
TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force); TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force);
String calculateImageEtag(byte[] imageData); String calculateImageEtag(byte[] imageData);

3
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 com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Dashboard; 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.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -42,6 +43,8 @@ public interface ResourceService extends EntityDaoService {
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
ResourceExportData exportResource(TbResourceInfo resourceInfo);
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId); TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey); TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);

1
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) @JsonInclude(JsonInclude.Include.NON_NULL)
public class ResourceExportData { public class ResourceExportData {
private String etag;
private String title; private String title;
private ResourceType type; private ResourceType type;
private ResourceSubType subType; private ResourceSubType subType;

22
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java

@ -15,11 +15,16 @@
*/ */
package org.thingsboard.server.common.data.widget; 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.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import java.util.Optional;
@Data @Data
public class WidgetType extends BaseWidgetType { public class WidgetType extends BaseWidgetType {
@ -43,4 +48,21 @@ public class WidgetType extends BaseWidgetType {
this.descriptor = widgetType.getDescriptor(); 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());
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -33,7 +33,7 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @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<WidgetTypeId> { public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> {
@Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")

27
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.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; 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.databind.type.CollectionType;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@ -469,6 +470,22 @@ public class JacksonUtil {
} }
public static void replaceAllByMapping(JsonNode jsonNode, Map<String, String> mapping, Map<String, String> templateParams, BiFunction<String, String, String> processor) { public static void replaceAllByMapping(JsonNode jsonNode, Map<String, String> mapping, Map<String, String> templateParams, BiFunction<String, String, String> 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<String, String> mapping, Map<String, String> templateParams, BiFunction<String, JsonNode, JsonNode> processor) {
for (var entry : mapping.entrySet()) { for (var entry : mapping.entrySet()) {
String expression = entry.getValue(); String expression = entry.getValue();
Queue<JsonPathProcessingTask> tasks = new LinkedList<>(); Queue<JsonPathProcessingTask> tasks = new LinkedList<>();
@ -515,15 +532,7 @@ public class JacksonUtil {
for (var replacement : task.getVariables().entrySet()) { for (var replacement : task.getVariables().entrySet()) {
name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue()));
} }
if (node.isObject() && value.isTextual()) { ((ObjectNode) node).set(token, processor.apply(name, value));
((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()));
}
}
} else { } else {
if (StringUtils.isNotEmpty(variableName)) { if (StringUtils.isNotEmpty(variableName)) {
tasks.add(task.next(value, variableName, variableValue)); tasks.add(task.next(value, variableName, variableValue));

78
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.EntityType;
import org.thingsboard.server.common.data.HasImage; import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.ImageDescriptor; 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.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult; import org.thingsboard.server.common.data.TbImageDeleteResult;
@ -62,7 +63,6 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@ -75,7 +75,6 @@ import static org.apache.commons.lang3.ArrayUtils.get;
public class BaseImageService extends BaseResourceService implements ImageService { public class BaseImageService extends BaseResourceService implements ImageService {
private static final int MAX_ENTITIES_TO_FIND = 10; private static final int MAX_ENTITIES_TO_FIND = 10;
private static final String DEFAULT_CONFIG_TAG = "defaultConfig";
public static Map<String, String> DASHBOARD_BASE64_MAPPING = new HashMap<>(); public static Map<String, String> DASHBOARD_BASE64_MAPPING = new HashMap<>();
public static Map<String, String> WIDGET_TYPE_BASE64_MAPPING = new HashMap<>(); public static Map<String, String> WIDGET_TYPE_BASE64_MAPPING = new HashMap<>();
@ -232,6 +231,24 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return resourceDao.getResourcePreview(tenantId, imageId); 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 @Override
public TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force) { public TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force) {
var tenantId = imageInfo.getTenantId(); var tenantId = imageInfo.getTenantId();
@ -299,13 +316,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
entity.setImage(result.getValue()); entity.setImage(result.getValue());
boolean updated = result.isUpdated(); boolean updated = result.isUpdated();
if (entity.getDescriptor().isObject()) { if (entity.getDescriptor().isObject()) {
ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); JsonNode defaultConfig = entity.getDefaultConfig();
JsonNode defaultConfig = Optional.ofNullable(descriptor.get(DEFAULT_CONFIG_TAG))
.filter(JsonNode::isTextual).map(JsonNode::asText)
.map(JacksonUtil::toJsonNode).orElse(null);
if (defaultConfig != null) { if (defaultConfig != null) {
updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); 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()); updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor());
@ -341,7 +355,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
return base64ToImageUrl(tenantId, name, data, false); 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) { private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) {
if (StringUtils.isBlank(data)) { if (StringUtils.isBlank(data)) {
@ -352,7 +366,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String resourceName = null; String resourceName = null;
String resourceSubType = null; String resourceSubType = null;
String etag = null; String etag = null;
String mediaType; String mediaType = null;
var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data); var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data);
if (matcher.matches()) { if (matcher.matches()) {
String[] metadata = matcher.group(1).split(":"); String[] metadata = matcher.group(1).split(":");
@ -361,6 +375,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
resourceSubType = decode(get(metadata, 2)); resourceSubType = decode(get(metadata, 2));
etag = get(metadata, 3); etag = get(metadata, 3);
mediaType = matcher.group(2); 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/"))) { } else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) {
mediaType = StringUtils.substringBetween(data, "data:", ";base64"); mediaType = StringUtils.substringBetween(data, "data:", ";base64");
} else { } else {
@ -370,6 +386,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic
String base64Data = StringUtils.substringAfter(data, "base64,"); String base64Data = StringUtils.substringAfter(data, "base64,");
byte[] imageData = StringUtils.isNotEmpty(base64Data) ? Base64.getDecoder().decode(base64Data) : null; byte[] imageData = StringUtils.isNotEmpty(base64Data) ? Base64.getDecoder().decode(base64Data) : null;
if (StringUtils.isBlank(etag)) { if (StringUtils.isBlank(etag)) {
if (imageData == null) {
return UpdateResult.of(false, data);
}
etag = calculateEtag(imageData); etag = calculateEtag(imageData);
} }
var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag); var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag);
@ -454,14 +473,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic
inlineImage(widgetTypeDetails, images); inlineImage(widgetTypeDetails, images);
ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor(); ObjectNode descriptor = (ObjectNode) widgetTypeDetails.getDescriptor();
inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor, images); inlineIntoJson(widgetTypeDetails.getTenantId(), descriptor, images);
if (descriptor.has(DEFAULT_CONFIG_TAG) && descriptor.get(DEFAULT_CONFIG_TAG).isTextual()) { JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
try { if (defaultConfig != null) {
var defaultConfig = JacksonUtil.toJsonNode(descriptor.get(DEFAULT_CONFIG_TAG).asText()); inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images);
inlineIntoJson(widgetTypeDetails.getTenantId(), defaultConfig, images); widgetTypeDetails.setDefaultConfig(defaultConfig);
descriptor.put(DEFAULT_CONFIG_TAG, JacksonUtil.toString(defaultConfig));
} catch (Exception e) {
log.debug("[{}][{}] Failed to process default config: ", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), e);
}
} }
return images; 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<TbResourceInfo> processedImages) { private String inlineImage(TenantId tenantId, String path, String url, boolean addTbImagePrefix, List<TbResourceInfo> processedImages) {
return inlineImage(tenantId, path, url, (key, imageInfo) -> { return inlineImage(tenantId, path, url, (key, imageInfo) -> {
String tbImagePrefix = ""; ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
boolean addData = true; String value = "";
if (addTbImagePrefix) { if (addTbImagePrefix) {
tbImagePrefix = "tb-image:" + encode(imageInfo.getResourceKey()) + ":" value = "tb-image:";
+ encode(imageInfo.getName()) + ":"
+ encode(imageInfo.getResourceSubType().name()) + ":"
+ imageInfo.getEtag() + ";";
if (processedImages != null && !key.isPreview()) { if (processedImages != null && !key.isPreview()) { // images are stored separately
addData = false;
processedImages.add(imageInfo); processedImages.add(imageInfo);
value += descriptor.getEtag();
return value;
} }
}
byte[] data; value += encode(imageInfo.getResourceKey()) + ":"
if (addData) { + encode(imageInfo.getName()) + ":"
data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId()); + encode(imageInfo.getResourceSubType().name()) + ":"
} else { + imageInfo.getEtag() + ";";
data = null;
} }
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);
}); });
} }

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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.resource; package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; 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.Dashboard;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; 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.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -81,11 +83,13 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
private ImageService imageService; private ImageService imageService;
private static final Map<String, String> DASHBOARD_RESOURCES_MAPPING = Map.of( private static final Map<String, String> DASHBOARD_RESOURCES_MAPPING = Map.of(
"widgets.*.config.actions.*.*.customResources.*.url.id", "" "widgets.*.config.actions.*.*.customResources.*.url", ""
); );
private static final Map<String, String> WIDGET_RESOURCES_MAPPING = Map.of( private static final Map<String, String> WIDGET_RESOURCES_MAPPING = Map.of(
"resources.*.url.id", "" "resources.*.url", ""
);
private static final Map<String, String> WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING = Map.of(
"actions.*.*.customResources.*.url", ""
); );
@Override @Override
@ -180,6 +184,21 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.getResourceData(tenantId, resourceId); return resourceDao.getResourceData(tenantId, resourceId);
} }
@Override
public ResourceExportData exportResource(TbResourceInfo resourceInfo) {
byte[] data = 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))
.etag(resourceInfo.getEtag())
.build();
}
@Override @Override
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
@ -298,7 +317,13 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override @Override
public boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails) { public boolean replaceResourcesUsageWithUrls(WidgetTypeDetails widgetTypeDetails) {
return replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); boolean updated = replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING);
JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig();
if (defaultConfig != null) {
updated |= replaceResourcesUsageWithUrls(widgetTypeDetails.getTenantId(), defaultConfig, WIDGET_DEFAULT_CONFIG_RESOURCES_MAPPING);
widgetTypeDetails.setDefaultConfig(defaultConfig);
}
return updated;
} }
private boolean replaceResourcesUsageWithUrls(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) { private boolean replaceResourcesUsageWithUrls(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
@ -314,7 +339,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
if (metadata.length < 2) { if (metadata.length < 2) {
return value; return value;
} }
ResourceType resourceType = ResourceType.valueOf(decode(metadata[0])); ResourceType resourceType = ResourceType.valueOf(metadata[0]);
String etag = metadata[1]; String etag = metadata[1];
resourceInfo = findSystemOrTenantResourceByEtag(tenantId, resourceType, etag); resourceInfo = findSystemOrTenantResourceByEtag(tenantId, resourceType, etag);
@ -350,7 +375,13 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override @Override
public List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails) { public List<TbResourceInfo> replaceResourcesUrlsWithTags(WidgetTypeDetails widgetTypeDetails) {
return replaceResourcesUrlsWithTags(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor(), WIDGET_RESOURCES_MAPPING); List<TbResourceInfo> 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<TbResourceInfo> replaceResourcesUrlsWithTags(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) { private List<TbResourceInfo> replaceResourcesUrlsWithTags(TenantId tenantId, JsonNode jsonNode, Map<String, String> mapping) {
@ -377,7 +408,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
TbResourceInfo resourceInfo = findResourceInfoByTenantIdAndKey(resourceTenantId, resourceType, resourceKey); TbResourceInfo resourceInfo = findResourceInfoByTenantIdAndKey(resourceTenantId, resourceType, resourceKey);
if (resourceInfo != null) { if (resourceInfo != null) {
resources.add(resourceInfo); resources.add(resourceInfo);
return "tb-resource:" + String.join(":", encode(resourceType.name()), resourceInfo.getEtag()); return "tb-resource:" + String.join(":", resourceType.name(), resourceInfo.getEtag());
} else { } else {
return value; return value;
} }
@ -386,12 +417,25 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
} }
private void processResources(JsonNode jsonNode, Map<String, String> mapping, UnaryOperator<String> processor) { private void processResources(JsonNode jsonNode, Map<String, String> mapping, UnaryOperator<String> processor) {
JacksonUtil.replaceAllByMapping(jsonNode, mapping, Collections.emptyMap(), (name, value) -> { JacksonUtil.replaceByMapping(jsonNode, mapping, Collections.emptyMap(), (name, urlNode) -> {
if (StringUtils.isBlank(value)) { String value = null;
return value; 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; return newValue;
}); });
} }

Loading…
Cancel
Save