diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java index 6bc786d943..399aabaa8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java @@ -18,15 +18,11 @@ package org.thingsboard.server.service.install.update; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.asset.AssetProfile; -import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.HasImage; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.device.DeviceProfileDao; @@ -34,6 +30,9 @@ import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; +import java.util.function.BiFunction; +import java.util.function.Function; + @Component @RequiredArgsConstructor @Slf4j @@ -48,108 +47,78 @@ public class ImagesUpdater { public void updateWidgetsBundlesImages() { log.info("Updating widgets bundles images..."); var widgetsBundles = new PageDataIterable<>(widgetsBundleDao::findAllWidgetsBundles, 128); - int updatedCount = 0; - int totalCount = 0; - for (WidgetsBundle widgetsBundle : widgetsBundles) { - totalCount++; - try { - boolean updated = imageService.replaceBase64WithImageUrl(widgetsBundle, "bundle"); - if (updated) { - widgetsBundleDao.save(widgetsBundle.getTenantId(), widgetsBundle); - log.debug("[{}][{}][{}] Updated widgets bundle images", widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle.getTitle()); - updatedCount++; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to update widgets bundle images", widgetsBundle.getTenantId(), widgetsBundle.getId(), widgetsBundle.getTitle(), e); - } - } - log.info("Updated {} widgets bundles out of {}", updatedCount, totalCount); + updateImages(widgetsBundles, "bundle", imageService::replaceBase64WithImageUrl, widgetsBundleDao); } public void updateWidgetTypesImages() { log.info("Updating widget types images..."); - var widgetTypes = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024); - int updatedCount = 0; - int totalCount = 0; - for (WidgetTypeId widgetTypeId : widgetTypes) { - totalCount++; - WidgetTypeDetails widgetTypeDetails = widgetTypeDao.findById(TenantId.SYS_TENANT_ID, widgetTypeId.getId()); - try { - boolean updated = imageService.replaceBase64WithImageUrl(widgetTypeDetails); - if (updated) { - widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); - log.debug("[{}][{}][{}] Updated widget type images", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), widgetTypeDetails.getName()); - updatedCount++; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to update widget type images", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId(), widgetTypeDetails.getName(), e); - } - } - log.info("Updated {} widget types out of {}", updatedCount, totalCount); + var widgetTypesIds = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024); + updateImages(widgetTypesIds, "widget type", imageService::replaceBase64WithImageUrl, widgetTypeDao); } public void updateDashboardsImages() { log.info("Updating dashboards images..."); - var dashboards = new PageDataIterable<>(dashboardDao::findAllIds, 1024); - int updatedCount = 0; - int totalCount = 0; - for (DashboardId dashboardId : dashboards) { - totalCount++; - Dashboard dashboard = dashboardDao.findById(TenantId.SYS_TENANT_ID, dashboardId.getId()); - try { - boolean updated = imageService.replaceBase64WithImageUrl(dashboard); - if (updated) { - dashboardDao.save(dashboard.getTenantId(), dashboard); - log.info("[{}][{}][{}] Updated dashboard images", dashboard.getTenantId(), dashboardId, dashboard.getTitle()); - updatedCount++; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to update dashboard images", dashboard.getTenantId(), dashboardId, dashboard.getTitle(), e); - } - } - log.info("Updated {} dashboards out of {}", updatedCount, totalCount); + var dashboardsIds = new PageDataIterable<>(dashboardDao::findAllIds, 1024); + updateImages(dashboardsIds, "dashboard", imageService::replaceBase64WithImageUrl, dashboardDao); } public void updateDeviceProfilesImages() { log.info("Updating device profiles images..."); var deviceProfiles = new PageDataIterable<>(deviceProfileDao::findAll, 256); - int updatedCount = 0; - int totalCount = 0; - for (DeviceProfile deviceProfile : deviceProfiles) { - totalCount++; - try { - boolean updated = imageService.replaceBase64WithImageUrl(deviceProfile, "device profile"); - if (updated) { - deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile); - log.debug("[{}][{}][{}] Updated device profile images", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName()); - updatedCount++; - } - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to update device profile images", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName(), e); - } - } - log.info("Updated {} device profiles out of {}", updatedCount, totalCount); + updateImages(deviceProfiles, "device profile", imageService::replaceBase64WithImageUrl, deviceProfileDao); } public void updateAssetProfilesImages() { log.info("Updating asset profiles images..."); var assetProfiles = new PageDataIterable<>(assetProfileDao::findAll, 256); + updateImages(assetProfiles, "asset profile", imageService::replaceBase64WithImageUrl, assetProfileDao); + } + + private void updateImages(Iterable entities, String type, + BiFunction updater, Dao dao) { int updatedCount = 0; int totalCount = 0; - for (AssetProfile assetProfile : assetProfiles) { + for (E entity : entities) { totalCount++; try { - boolean updated = imageService.replaceBase64WithImageUrl(assetProfile, "asset profile"); + boolean updated = updater.apply(entity, type); if (updated) { - assetProfileDao.save(assetProfile.getTenantId(), assetProfile); - log.debug("[{}][{}][{}] Updated asset profile images", assetProfile.getTenantId(), assetProfile.getId(), assetProfile.getName()); + dao.save(entity.getTenantId(), entity); + log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type); updatedCount++; } } catch (Exception e) { - log.error("[{}][{}][{}] Failed to update asset profile images", assetProfile.getTenantId(), assetProfile.getId(), assetProfile.getName(), e); + log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e); + } + if (totalCount % 100 == 0) { + log.info("Processed {} {}s so far", totalCount, type); } } - log.info("Updated {} asset profiles out of {}", updatedCount, totalCount); + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); + } + + private void updateImages(Iterable entitiesIds, String type, + Function updater, Dao dao) { + int updatedCount = 0; + int totalCount = 0; + for (EntityId id : entitiesIds) { + totalCount++; + E entity = dao.findById(TenantId.SYS_TENANT_ID, id.getId()); + try { + boolean updated = updater.apply(entity); + if (updated) { + dao.save(entity.getTenantId(), entity); + log.debug("[{}][{}] Updated {} images", entity.getTenantId(), entity.getName(), type); + updatedCount++; + } + } catch (Exception e) { + log.error("[{}][{}] Failed to update {} images", entity.getTenantId(), entity.getName(), type, e); + } + if (totalCount % 100 == 0) { + log.info("Processed {} {}s so far", totalCount, type); + } + } + log.info("Updated {} {}s out of {}", updatedCount, type, totalCount); } } diff --git a/dao/pom.xml b/dao/pom.xml index 696a059f4c..d3145fe892 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -242,6 +242,10 @@ org.apache.xmlgraphics batik-codec + + com.drewnoakes + metadata-extractor + 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 bc84a9533a..4f08c8f494 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 @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; import lombok.Data; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -285,8 +286,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic if (entity.getDescriptor().isObject()) { ObjectNode descriptor = (ObjectNode) entity.getDescriptor(); JsonNode defaultConfig = JacksonUtil.toJsonNode(descriptor.get("defaultConfig").asText()); - updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); - descriptor.put("defaultConfig", defaultConfig.toString()); + if (defaultConfig != null && defaultConfig.isObject()) { + updated |= base64ToImageUrlUsingMapping(entity.getTenantId(), WIDGET_TYPE_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), defaultConfig); + descriptor.put("defaultConfig", defaultConfig.toString()); + } } updated |= base64ToImageUrlRecursively(entity.getTenantId(), prefix, entity.getDescriptor()); return updated; @@ -313,6 +316,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic JsonPathProcessingTask task = tasks.poll(); String token = task.currentToken(); JsonNode node = task.getNode(); + if (node == null) { + continue; + } if (token.equals("*") || token.startsWith("$")) { String variableName = token.startsWith("$") ? token.substring(1) : null; if (node.isArray()) { @@ -345,8 +351,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic } if (task.isLast()) { String name = expression; - for (var replacements : task.getVariables().entrySet()) { - name = name.replace("$" + replacements.getKey(), replacements.getValue()); + for (var replacement : task.getVariables().entrySet()) { + name = name.replace("$" + replacement.getKey(), Strings.nullToEmpty(replacement.getValue())); } if (node.isObject() && value.isTextual()) { var result = base64ToImageUrl(tenantId, name, value.asText()); @@ -362,7 +368,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic } } } else { - if (StringUtils.isNotEmpty(variableName) && StringUtils.isNotEmpty(variableValue)) { + if (StringUtils.isNotEmpty(variableName)) { tasks.add(task.next(value, variableName, variableValue)); } else { tasks.add(task.next(value)); @@ -404,6 +410,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic byte[] imageData = Base64.getDecoder().decode(base64Data); String etag = calculateEtag(imageData); var imageInfo = findImageByTenantIdAndEtag(tenantId, etag); + if (imageInfo == null && !tenantId.isSysTenantId()) { + imageInfo = findImageByTenantIdAndEtag(TenantId.SYS_TENANT_ID, etag); + } if (imageInfo == null) { TbResource image = new TbResource(); image.setTenantId(tenantId); @@ -415,9 +424,9 @@ public class BaseImageService extends BaseResourceService implements ImageServic String fileName; if (StringUtils.isBlank(mdResourceKey)) { - fileName = mdResourceName.toLowerCase() - .replace("'", "").replace("\"", "") - .replace(" ", "_").replace("/", "_") + fileName = StringUtils.strip(mdResourceName.toLowerCase() + .replaceAll("['\"]", "") + .replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers + "." + extension; } else { fileName = mdResourceKey; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java index 7335d6e072..3adc50d2aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java @@ -43,7 +43,7 @@ public interface DashboardRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); - @Query("SELECT id FROM DashboardEntity") + @Query("SELECT d.id FROM DashboardEntity d") Page findAllIds(Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 5cb32c0c10..94ea9c1a90 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -91,7 +91,7 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index 7536133643..0583d0b47d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java @@ -69,17 +69,17 @@ public interface WidgetTypeRepository extends JpaRepository> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))", - nativeQuery = true) + nativeQuery = true) List findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId, - @Param("resourceId") UUID resourceId); + @Param("resourceId") UUID resourceId); @Query("SELECT externalId FROM WidgetTypeDetailsEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); - @Query("SELECT id FROM WidgetTypeDetailsEntity") + @Query("SELECT w.id FROM WidgetTypeDetailsEntity w") Page findAllIds(Pageable pageable); - @Query("SELECT id FROM WidgetTypeDetailsEntity WHERE tenantId = :tenantId") + @Query("SELECT w.id FROM WidgetTypeDetailsEntity w WHERE w.tenantId = :tenantId") Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java index 104a2cf6ee..d6905b13ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java @@ -15,9 +15,14 @@ */ package org.thingsboard.server.dao.util; +import com.drew.imaging.ImageMetadataReader; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; import lombok.AccessLevel; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.batik.anim.dom.SAXSVGDocumentFactory; import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.DocumentLoader; @@ -41,11 +46,13 @@ import java.io.ByteArrayOutputStream; import java.util.Map; @NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j public class ImageUtils { private static final Map mediaTypeMappings = Map.of( "jpeg", "jpg", - "svg+xml", "svg" + "svg+xml", "svg", + "x-icon", "ico" ); public static String mediaTypeToFileExtension(String mimeType) { @@ -64,15 +71,49 @@ public class ImageUtils { if (mediaTypeToFileExtension(mediaType).equals("svg")) { return processSvgImage(data, mediaType, thumbnailMaxDimension); } - - BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(data)); ProcessedImage image = new ProcessedImage(); image.setMediaType(mediaType); - image.setWidth(bufferedImage.getWidth()); - image.setHeight(bufferedImage.getHeight()); image.setData(data); image.setSize(data.length); + BufferedImage bufferedImage = null; + try { + bufferedImage = ImageIO.read(new ByteArrayInputStream(data)); + } catch (Exception ignored) { + } + if (bufferedImage == null) { // means that media type is not supported by ImageIO; extracting width and height from metadata and leaving preview as original image + Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(data)); + for (Directory dir : metadata.getDirectories()) { + Tag widthTag = dir.getTags().stream() + .filter(tag -> tag.getTagName().toLowerCase().contains("width")) + .findFirst().orElse(null); + Tag heightTag = dir.getTags().stream() + .filter(tag -> tag.getTagName().toLowerCase().contains("height")) + .findFirst().orElse(null); + if (widthTag == null || heightTag == null) { + continue; + } + int width = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); + int height = Integer.parseInt(dir.getObject(widthTag.getTagType()).toString()); + image.setWidth(width); + image.setHeight(height); + + ProcessedImage preview = new ProcessedImage(); + preview.setWidth(image.getWidth()); + preview.setHeight(image.getHeight()); + preview.setMediaType(mediaType); + preview.setData(null); + preview.setSize(data.length); + image.setPreview(preview); + log.debug("Couldn't parse {} ({}) with ImageIO, got width {} and height {} from metadata", mediaType, dir.getName(), width, height); + return image; + } + throw new IllegalArgumentException("Media type " + mediaType + " not supported"); + } + + image.setWidth(bufferedImage.getWidth()); + image.setHeight(bufferedImage.getHeight()); + ProcessedImage preview = new ProcessedImage(); int[] thumbnailDimensions = getThumbnailDimensions(image.getWidth(), image.getHeight(), thumbnailMaxDimension); preview.setWidth(thumbnailDimensions[0]); @@ -101,18 +142,19 @@ public class ImageUtils { } public static ProcessedImage processSvgImage(byte[] data, String mediaType, int thumbnailMaxDimension) throws Exception { - SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( - XMLResourceDescriptor.getXMLParserClassName()); - Document document = factory.createDocument( - null, new ByteArrayInputStream(data)); + SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()); + Document document = factory.createDocument(null, new ByteArrayInputStream(data)); Integer width = null; Integer height = null; String strWidth = document.getDocumentElement().getAttribute("width"); String strHeight = document.getDocumentElement().getAttribute("height"); if (StringUtils.isNotEmpty(strWidth) && StringUtils.isNotEmpty(strHeight)) { - width = (int) Double.parseDouble(strWidth); - height = (int) Double.parseDouble(strHeight);; - } else { + try { + width = (int) Double.parseDouble(strWidth); + height = (int) Double.parseDouble(strHeight); + } catch (NumberFormatException ignored) {} // in case width and height are in %, mm, etc. + } + if (width == null || height == null) { String viewBox = document.getDocumentElement().getAttribute("viewBox"); if (StringUtils.isNotEmpty(viewBox)) { String[] viewBoxValues = viewBox.split(" "); @@ -124,10 +166,10 @@ public class ImageUtils { } if (width == null) { UserAgent agent = new UserAgentAdapter(); - DocumentLoader loader= new DocumentLoader(agent); + DocumentLoader loader = new DocumentLoader(agent); BridgeContext context = new BridgeContext(agent, loader); context.setDynamic(true); - GVTBuilder builder= new GVTBuilder(); + GVTBuilder builder = new GVTBuilder(); GraphicsNode root = builder.build(context, document); var bounds = root.getPrimitiveBounds(); if (bounds != null) { diff --git a/pom.xml b/pom.xml index 9fe71b0ae8..2dca3c54f7 100755 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,7 @@ 6.4.2 1.34.1 1.17 + 2.19.0 @@ -2040,6 +2041,11 @@ batik-codec ${apache-xmlgraphics.version} + + com.drewnoakes + metadata-extractor + ${drewnoakes-metadata-extractor.version} +