Browse Source

Merge pull request #9542 from thingsboard/feature/image-resources

Image gallery
pull/9770/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
0f98c480b0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      application/src/main/data/json/system/widget_types/image_map.json
  2. 2
      application/src/main/data/json/system/widget_types/label_widget.json
  3. 2
      application/src/main/data/json/system/widget_types/markers_placement___image_map.json
  4. 37
      application/src/main/data/upgrade/3.6.1/schema_update.sql
  5. 14
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  6. 6
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  7. 5
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  8. 14
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  9. 14
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  10. 283
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  11. 128
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  12. 1
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 26
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  14. 18
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  15. 10
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  16. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV1.java
  17. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java
  19. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
  21. 60
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  22. 120
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  23. 133
      application/src/main/java/org/thingsboard/server/service/install/update/ImagesUpdater.java
  24. 16
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  25. 155
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java
  26. 130
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  27. 38
      application/src/main/java/org/thingsboard/server/service/resource/TbImageService.java
  28. 18
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  29. 10
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
  30. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
  31. 36
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/ResourceExportService.java
  32. 83
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java
  33. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java
  34. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  35. 7
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  36. 10
      application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
  37. 6
      application/src/main/resources/thingsboard.yml
  38. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  39. 277
      application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java
  40. 118
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  41. 7
      application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java
  42. 3
      application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
  43. 90
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  44. 4
      application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java
  45. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  46. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
  47. 58
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ImageService.java
  48. 7
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  49. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  50. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  51. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java
  52. 24
      common/data/src/main/java/org/thingsboard/server/common/data/HasImage.java
  53. 30
      common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java
  54. 33
      common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java
  55. 17
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  56. 4
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  57. 32
      common/data/src/main/java/org/thingsboard/server/common/data/TbImageDeleteResult.java
  58. 69
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  59. 77
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  60. 4
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
  61. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java
  62. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
  63. 1
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  64. 5
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  65. 7
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
  66. 7
      common/proto/src/main/proto/queue.proto
  67. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  68. 11
      common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java
  69. 12
      dao/pom.xml
  70. 6
      dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java
  71. 29
      dao/src/main/java/org/thingsboard/server/dao/ImageContainerDao.java
  72. 2
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
  73. 6
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java
  74. 5
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  75. 2
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java
  76. 7
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java
  77. 7
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  78. 6
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java
  79. 5
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  80. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  81. 31
      dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java
  82. 30
      dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java
  83. 615
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  84. 82
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  85. 28
      dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java
  86. 13
      dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
  87. 14
      dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
  88. 5
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  89. 52
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  90. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java
  91. 18
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java
  92. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
  93. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java
  94. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java
  95. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
  96. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  97. 18
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  98. 41
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java
  99. 46
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java
  100. 31
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java

2
application/src/main/data/json/system/widget_types/image_map.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/label_widget.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/markers_placement___image_map.json

File diff suppressed because one or more lines are too long

37
application/src/main/data/upgrade/3.6.1/schema_update.sql

@ -0,0 +1,37 @@
--
-- Copyright © 2016-2023 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- RESOURCES UPDATE START
DO
$$
BEGIN
IF NOT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'resource' AND column_name = 'data' AND data_type = 'bytea') THEN
ALTER TABLE resource RENAME COLUMN data TO base64_data;
ALTER TABLE resource ADD COLUMN data bytea;
UPDATE resource SET data = decode(base64_data, 'base64') WHERE base64_data IS NOT NULL;
ALTER TABLE resource DROP COLUMN base64_data;
END IF;
END;
$$;
ALTER TABLE resource ADD COLUMN IF NOT EXISTS descriptor varchar;
ALTER TABLE resource ADD COLUMN IF NOT EXISTS preview bytea;
ALTER TABLE resource ADD COLUMN IF NOT EXISTS external_id uuid;
CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);
-- RESOURCES UPDATE END

14
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService;
import org.thingsboard.server.service.security.permission.Operation;
@ -45,6 +46,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFIL
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -64,6 +67,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class AssetProfileController extends BaseController {
private final TbAssetProfileService tbAssetProfileService;
private final ImageService imageService;
@ApiOperation(value = "Get Asset Profile (getAssetProfileById)",
notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " +
@ -74,10 +78,16 @@ public class AssetProfileController extends BaseController {
@ResponseBody
public AssetProfile getAssetProfileById(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
return checkAssetProfileId(assetProfileId, Operation.READ);
var result = checkAssetProfileId(assetProfileId, Operation.READ);
if (inlineImages) {
imageService.inlineImage(result);
}
return result;
}
@ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)",

6
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -124,6 +124,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator;
@ -145,7 +146,6 @@ import org.thingsboard.server.service.entitiy.user.TbUserSettingsService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
@ -272,7 +272,7 @@ public abstract class BaseController {
protected PartitionService partitionService;
@Autowired
protected TbResourceService resourceService;
protected ResourceService resourceService;
@Autowired
protected OtaPackageService otaPackageService;
@ -587,7 +587,7 @@ public abstract class BaseController {
checkWidgetTypeId(new WidgetTypeId(entityId.getId()), operation);
return;
case TB_RESOURCE:
checkResourceId(new TbResourceId(entityId.getId()), operation);
checkResourceInfoId(new TbResourceId(entityId.getId()), operation);
return;
case OTA_PACKAGE:
checkOtaPackageId(new OtaPackageId(entityId.getId()), operation);

5
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -32,6 +32,9 @@ public class ControllerConstants {
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. ";
protected static final String INLINE_IMAGES = "inlineImages";
protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)";
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
@ -142,6 +145,8 @@ public class ControllerConstants {
protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority.";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId";
protected static final String RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES = "LWM2M_MODEL, JKS, PKCS_12, JS_MODULE";

14
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -74,6 +75,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -94,6 +97,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class DashboardController extends BaseController {
private final TbDashboardService tbDashboardService;
private final ImageService imageService;
public static final String DASHBOARD_ID = "dashboardId";
private static final String HOME_DASHBOARD_ID = "homeDashboardId";
@ -153,10 +157,16 @@ public class DashboardController extends BaseController {
@ResponseBody
public Dashboard getDashboardById(
@ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
@PathVariable(DASHBOARD_ID) String strDashboardId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException {
checkParameter(DASHBOARD_ID, strDashboardId);
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
return checkDashboardId(dashboardId, Operation.READ);
var result = checkDashboardId(dashboardId, Operation.READ);
if (inlineImages) {
imageService.inlineImages(result);
}
return result;
}
@ApiOperation(value = "Create Or Update Dashboard (saveDashboard)",

14
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService;
@ -52,6 +53,8 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -72,6 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
public class DeviceProfileController extends BaseController {
private final TbDeviceProfileService tbDeviceProfileService;
private final ImageService imageService;
@Autowired
private TimeseriesService timeseriesService;
@ -85,10 +89,16 @@ public class DeviceProfileController extends BaseController {
@ResponseBody
public DeviceProfile getDeviceProfileById(
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException {
checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
return checkDeviceProfileId(deviceProfileId, Operation.READ);
var result = checkDeviceProfileId(deviceProfileId, Operation.READ);
if (inlineImages) {
imageService.inlineImage(result);
}
return result;
}
@ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)",

283
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -0,0 +1,283 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
@Slf4j
@RestController
@TbCoreComponent
@RequiredArgsConstructor
public class ImageController extends BaseController {
private final ImageService imageService;
private final TbImageService tbImageService;
@Value("${cache.image.systemImagesBrowserTtlInMinutes:0}")
private int systemImagesBrowserTtlInMinutes;
@Value("${cache.image.tenantImagesBrowserTtlInMinutes:0}")
private int tenantImagesBrowserTtlInMinutes;
private static final String IMAGE_URL = "/api/images/{type}/{key}";
private static final String SYSTEM_IMAGE = "system";
private static final String TENANT_IMAGE = "tenant";
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping("/api/image")
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
@RequestPart(required = false) String title) throws Exception {
SecurityUser user = getCurrentUser();
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
image.setFileName(file.getOriginalFilename());
if (StringUtils.isNotEmpty(title)) {
image.setTitle(title);
} else {
image.setTitle(file.getOriginalFilename());
}
image.setResourceType(ResourceType.IMAGE);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(file.getContentType());
image.setDescriptorValue(descriptor);
image.setData(file.getBytes());
return tbImageService.save(image, user);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(IMAGE_URL)
public TbResourceInfo updateImage(@PathVariable String type,
@PathVariable String key,
@RequestPart MultipartFile file) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
TbResource image = new TbResource(imageInfo);
image.setData(file.getBytes());
image.setFileName(file.getOriginalFilename());
image.updateDescriptor(ImageDescriptor.class, descriptor -> {
descriptor.setMediaType(file.getContentType());
return descriptor;
});
return tbImageService.save(image, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(IMAGE_URL + "/info")
public TbResourceInfo updateImageInfo(@PathVariable String type,
@PathVariable String key,
@RequestBody TbResourceInfo newImageInfo) throws ThingsboardException {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.WRITE);
imageInfo.setTitle(newImageInfo.getTitle());
return tbImageService.save(imageInfo, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = IMAGE_URL, produces = "image/*")
public ResponseEntity<ByteArrayResource> downloadImage(@PathVariable String type,
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
return downloadIfChanged(type, key, etag, false);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = IMAGE_URL + "/export")
public ImageExportData exportImage(@PathVariable String type, @PathVariable String key) throws Exception {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
byte[] data = imageService.getImageData(imageInfo.getTenantId(), imageInfo.getId());
return new ImageExportData(descriptor.getMediaType(), imageInfo.getFileName(), imageInfo.getTitle(), imageInfo.getResourceKey(), Base64Utils.encodeToString(data));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping("/api/image/import")
public TbResourceInfo importImage(@RequestBody ImageExportData imageData) throws Exception {
SecurityUser user = getCurrentUser();
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
accessControlService.checkPermission(user, Resource.TB_RESOURCE, Operation.CREATE, null, image);
image.setFileName(imageData.getFileName());
if (StringUtils.isNotEmpty(imageData.getTitle())) {
image.setTitle(imageData.getTitle());
} else {
image.setTitle(imageData.getFileName());
}
image.setResourceKey(imageData.getResourceKey());
image.setResourceType(ResourceType.IMAGE);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(imageData.getMediaType());
image.setDescriptorValue(descriptor);
image.setData(Base64Utils.decodeFromString(imageData.getData()));
return tbImageService.save(image, user);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = IMAGE_URL + "/preview", produces = "image/png")
public ResponseEntity<ByteArrayResource> downloadImagePreview(@PathVariable String type,
@PathVariable String key,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws Exception {
return downloadIfChanged(type, key, etag, true);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(IMAGE_URL + "/info")
public TbResourceInfo getImageInfo(@PathVariable String type,
@PathVariable String key) throws ThingsboardException {
return checkImageInfo(type, key, Operation.READ);
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping("/api/images")
public PageData<TbResourceInfo> getImages(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION)
@RequestParam(required = false) boolean includeSystemImages,
@ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
// PE: generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TenantId tenantId = getTenantId();
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) {
return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink));
} else {
return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink));
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@DeleteMapping(IMAGE_URL)
public ResponseEntity<TbImageDeleteResult> deleteImage(@PathVariable String type,
@PathVariable String key,
@RequestParam(name = "force", required = false) boolean force) throws ThingsboardException {
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.DELETE);
TbImageDeleteResult result = tbImageService.delete(imageInfo, getCurrentUser(), force);
return (result.isSuccess() ? ResponseEntity.ok() : ResponseEntity.badRequest()).body(result);
}
private ResponseEntity<ByteArrayResource> downloadIfChanged(String type, String key, String etag, boolean preview) throws ThingsboardException, JsonProcessingException {
ImageCacheKey cacheKey = new ImageCacheKey(getTenantId(type), key, preview);
if (StringUtils.isNotEmpty(etag)) {
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbImageService.getETag(cacheKey))) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
}
}
TenantId tenantId = getTenantId();
TbResourceInfo imageInfo = checkImageInfo(type, key, Operation.READ);
String fileName = imageInfo.getFileName();
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
byte[] data;
if (preview) {
descriptor = descriptor.getPreviewDescriptor();
data = imageService.getImagePreview(tenantId, imageInfo.getId());
} else {
data = imageService.getImageData(tenantId, imageInfo.getId());
}
tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
.header("x-filename", fileName)
.header("Content-Type", descriptor.getMediaType())
.contentLength(data.length)
.eTag(descriptor.getEtag());
if (systemImagesBrowserTtlInMinutes > 0 && imageInfo.getTenantId().isSysTenantId()) {
result.cacheControl(CacheControl.maxAge(systemImagesBrowserTtlInMinutes, TimeUnit.MINUTES));
} else if (tenantImagesBrowserTtlInMinutes > 0 && !imageInfo.getTenantId().isSysTenantId()) {
result.cacheControl(CacheControl.maxAge(tenantImagesBrowserTtlInMinutes, TimeUnit.MINUTES));
} else {
result.cacheControl(CacheControl.noCache());
}
return result.body(new ByteArrayResource(data));
}
private TbResourceInfo checkImageInfo(String imageType, String key, Operation operation) throws ThingsboardException {
TenantId tenantId = getTenantId(imageType);
TbResourceInfo imageInfo = imageService.getImageInfoByTenantIdAndKey(tenantId, key);
checkEntity(getCurrentUser(), checkNotNull(imageInfo), operation);
return imageInfo;
}
private TenantId getTenantId(String imageType) throws ThingsboardException {
TenantId tenantId;
if (imageType.equals(TENANT_IMAGE)) {
tenantId = getTenantId();
} else if (imageType.equals(SYSTEM_IMAGE)) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
throw new IllegalArgumentException("Invalid image URL");
}
return tenantId;
}
}

128
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -19,6 +19,7 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
@ -26,16 +27,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
@ -50,8 +52,11 @@ import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Base64;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
@ -87,15 +92,14 @@ public class TbResourceController extends BaseController {
@ApiOperation(value = "Download Resource (downloadResource)", notes = "Download Resource based on the provided Resource Id." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException {
@GetMapping(value = "/resource/{resourceId}/download")
public ResponseEntity<ByteArrayResource> downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes()));
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
.header("x-filename", tbResource.getFileName())
@ -106,42 +110,38 @@ public class TbResourceController extends BaseController {
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET, produces = "application/xml")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
@GetMapping(value = "/resource/lwm2m/{resourceId}/download", produces = "application/xml")
public ResponseEntity<ByteArrayResource> downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag);
}
@ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-pkcs12")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
public ResponseEntity<ByteArrayResource> downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag);
}
@ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)",
notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET, produces = "application/x-java-keystore")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
@GetMapping(value = "/resource/jks/{resourceId}/download", produces = "application/x-java-keystore")
public ResponseEntity<ByteArrayResource> downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag);
}
@ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = DOWNLOAD_RESOURCE_IF_NOT_CHANGED + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET, produces = "application/javascript")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
@GetMapping(value = "/resource/js/{resourceId}/download", produces = "application/javascript")
public ResponseEntity<ByteArrayResource> downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId,
@RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag);
}
@ -150,8 +150,7 @@ public class TbResourceController extends BaseController {
RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/info/{resourceId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/resource/info/{resourceId}")
public TbResourceInfo getResourceInfoById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
@ -162,10 +161,10 @@ public class TbResourceController extends BaseController {
@ApiOperation(value = "Get Resource (getResourceById)",
notes = "Fetch the Resource object based on the provided Resource Id. " +
RESOURCE_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
produces = "application/json", hidden = true)
@Deprecated // resource's data should be fetched with a download request
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/resource/{resourceId}")
public TbResource getResourceById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
@ -184,13 +183,12 @@ public class TbResourceController extends BaseController {
produces = "application/json",
consumes = "application/json")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource", method = RequestMethod.POST)
@ResponseBody
public TbResource saveResource(@ApiParam(value = "A JSON value representing the Resource.")
@RequestBody TbResource resource) throws Exception {
@PostMapping(value = "/resource")
public TbResourceInfo saveResource(@ApiParam(value = "A JSON value representing the Resource.")
@RequestBody TbResource resource) throws Exception {
resource.setTenantId(getTenantId());
checkEntity(resource.getId(), resource, Resource.TB_RESOURCE);
return tbResourceService.save(resource, getCurrentUser());
return new TbResourceInfo(tbResourceService.save(resource, getCurrentUser()));
}
@ApiOperation(value = "Get Resource Infos (getResources)",
@ -198,8 +196,7 @@ public class TbResourceController extends BaseController {
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/resource")
public PageData<TbResourceInfo> getResources(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -215,9 +212,14 @@ public class TbResourceController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder();
filter.tenantId(getTenantId());
Set<ResourceType> resourceTypes = new HashSet<>();
if (StringUtils.isNotEmpty(resourceType)) {
filter.resourceType(ResourceType.valueOf(resourceType));
resourceTypes.add(ResourceType.valueOf(resourceType));
} else {
Collections.addAll(resourceTypes, ResourceType.values());
resourceTypes.remove(ResourceType.IMAGE);
}
filter.resourceTypes(resourceTypes);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink));
} else {
@ -225,13 +227,36 @@ public class TbResourceController extends BaseController {
}
}
@ApiOperation(value = "Get All Resource Infos (getAllResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/resource/tenant")
public PageData<TbResourceInfo> getTenantResources(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(getTenantId())
.resourceTypes(EnumSet.allOf(ResourceType.class))
.build();
return checkNotNull(resourceService.findTenantResourcesByTenantId(filter, pageLink));
}
@ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjectsPage)",
notes = "Returns a page of LwM2M objects parsed from Resources with type 'LWM2M_MODEL' owned by tenant or sysadmin. " +
PAGE_DATA_PARAMETERS + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/resource/lwm2m/page", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/resource/lwm2m/page")
public List<LwM2mObject> getLwm2mListObjectsPage(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@ -243,7 +268,7 @@ public class TbResourceController extends BaseController {
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = new PageLink(pageSize, page, textSearch);
return checkNotNull(resourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink));
return checkNotNull(tbResourceService.findLwM2mObjectPage(getTenantId(), sortProperty, sortOrder, pageLink));
}
@ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjects)",
@ -251,22 +276,20 @@ public class TbResourceController extends BaseController {
"You can specify parameters to filter the results. " + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/resource/lwm2m", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/resource/lwm2m")
public List<LwM2mObject> getLwm2mListObjects(@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES, required = true)
@RequestParam String sortOrder,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES, required = true)
@RequestParam String sortProperty,
@ApiParam(value = "LwM2M Object ids.", required = true)
@RequestParam(required = false) String[] objectIds) throws ThingsboardException {
return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
return checkNotNull(tbResourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds));
}
@ApiOperation(value = "Delete Resource (deleteResource)",
notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/resource/{resourceId}")
public void deleteResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable("resourceId") String strResourceId) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
@ -275,12 +298,12 @@ public class TbResourceController extends BaseController {
tbResourceService.delete(tbResource, getCurrentUser());
}
private ResponseEntity<org.springframework.core.io.Resource> downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException {
private ResponseEntity<ByteArrayResource> downloadResourceIfChanged(ResourceType resourceType, String strResourceId, String etag) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
etag = StringUtils.remove(etag, '\"'); // etag is wrapped in double quotes due to HTTP specification
if (etag.equals(tbResourceInfo.getEtag())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getEtag())
@ -289,15 +312,16 @@ public class TbResourceController extends BaseController {
}
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes()));
ByteArrayResource resource = new ByteArrayResource(tbResource.getData());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
.header("x-filename", tbResource.getFileName())
.contentLength(resource.contentLength())
.header("Content-Type", type.getMediaType())
.header("Content-Type", resourceType.getMediaType())
.cacheControl(CacheControl.noCache())
.eTag(tbResource.getEtag())
.body(resource);
}
}

1
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -133,6 +133,7 @@ public class TenantProfileController extends BaseController {
" \"maxRuleChains\": 0,\n" +
" \"maxResourcesInBytes\": 0,\n" +
" \"maxOtaPackagesInBytes\": 0,\n" +
" \"maxResourceSize\": 0,\n" +
" \"transportTenantMsgRateLimit\": \"1000:1,20000:60\",\n" +
" \"transportTenantTelemetryMsgRateLimit\": \"1000:1,20000:60\",\n" +
" \"transportTenantTelemetryDataPointsRateLimit\": \"1000:1,20000:60\",\n" +

26
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.security.permission.Operation;
@ -53,6 +54,8 @@ import java.util.Collections;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -72,6 +75,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_
public class WidgetTypeController extends AutoCommitController {
private final TbWidgetTypeService tbWidgetTypeService;
private final ImageService imageService;
private static final String WIDGET_TYPE_DESCRIPTION = "Widget Type represents the template for widget creation. Widget Type and Widget are similar to class and object in OOP theory.";
private static final String WIDGET_TYPE_DETAILS_DESCRIPTION = "Widget Type Details extend Widget Type and add image and description properties. " +
@ -92,10 +96,16 @@ public class WidgetTypeController extends AutoCommitController {
@ResponseBody
public WidgetTypeDetails getWidgetTypeById(
@ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
@PathVariable("widgetTypeId") String strWidgetTypeId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException {
checkParameter("widgetTypeId", strWidgetTypeId);
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
return checkWidgetTypeId(widgetTypeId, Operation.READ);
var result = checkWidgetTypeId(widgetTypeId, Operation.READ);
if (inlineImages) {
imageService.inlineImages(result);
}
return result;
}
@ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)",
@ -253,9 +263,16 @@ public class WidgetTypeController extends AutoCommitController {
@ResponseBody
public List<WidgetTypeDetails> getBundleWidgetTypesDetails(
@ApiParam(value = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages
) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId));
var result = checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId));
if (inlineImages) {
result.forEach(imageService::inlineImages);
}
return result;
}
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",
@ -369,6 +386,7 @@ public class WidgetTypeController extends AutoCommitController {
String typeFqn = fqn.substring(scopeQualifier.length() + 1);
WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, typeFqn);
checkNotNull(widgetType);
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, Operation.READ, widgetType.getId(), widgetType);
return widgetType;
}

18
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService;
import org.thingsboard.server.service.security.permission.Operation;
@ -47,6 +48,8 @@ import java.util.List;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -66,6 +69,7 @@ import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDL
public class WidgetsBundleController extends BaseController {
private final TbWidgetsBundleService tbWidgetsBundleService;
private final ImageService imageService;
private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. ";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types";
@ -78,10 +82,16 @@ public class WidgetsBundleController extends BaseController {
@ResponseBody
public WidgetsBundle getWidgetsBundleById(
@ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@PathVariable("widgetsBundleId") String strWidgetsBundleId,
@ApiParam(value = INLINE_IMAGES_DESCRIPTION)
@RequestParam(value = INLINE_IMAGES, required = false) boolean inlineImages) throws ThingsboardException {
checkParameter("widgetsBundleId", strWidgetsBundleId);
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkWidgetsBundleId(widgetsBundleId, Operation.READ);
var result = checkWidgetsBundleId(widgetsBundleId, Operation.READ);
if (inlineImages) {
imageService.inlineImage(result);
}
return result;
}
@ApiOperation(value = "Create Or Update Widget Bundle (saveWidgetsBundle)",
@ -196,9 +206,9 @@ public class WidgetsBundleController extends BaseController {
} else {
TenantId tenantId = getCurrentUser().getTenantId();
if (tenantOnly != null && tenantOnly) {
return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
} else {
return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
}
}
}

10
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -112,6 +112,8 @@ public class ThingsboardInstallService {
} else if ("3.0.1-cassandra".equals(upgradeFromVersion)) {
log.info("Migrating ThingsBoard latest timeseries data from cassandra to SQL database ...");
latestMigrateService.migrate();
} else if (upgradeFromVersion.equals("3.6.2-images")) {
installScripts.updateImages();
} else {
switch (upgradeFromVersion) {
case "1.2.3": //NOSONAR, Need to execute gradual upgrade starting from upgradeFromVersion
@ -268,6 +270,13 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("3.6.0");
case "3.6.1":
log.info("Upgrading ThingsBoard from version 3.6.1 to 3.6.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.1");
installScripts.loadSystemImages();
if (!getEnv("SKIP_IMAGES_MIGRATION", false)) {
installScripts.updateImages();
} else {
log.info("Skipping images migration. Run the upgrade with fromVersion as '3.6.2-images' to migrate");
}
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:
@ -321,6 +330,7 @@ public class ThingsboardInstallService {
// systemDataLoaderService.loadSystemPlugins();
// systemDataLoaderService.loadSystemRules();
installScripts.loadSystemLwm2mResources();
installScripts.loadSystemImages();
if (loadDemo) {
log.info("Loading demo data...");

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/resource/ResourceMsgConstructorV1.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.edge.rpc.constructor.resource;
import com.google.protobuf.ByteString;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TenantId;
@ -37,7 +38,7 @@ public class ResourceMsgConstructorV1 extends BaseResourceMsgConstructor {
.setResourceType(tbResource.getResourceType().name())
.setFileName(tbResource.getFileName());
if (tbResource.getData() != null) {
builder.setData(tbResource.getData());
builder.setData(tbResource.getEncodedData());
}
if (tbResource.getEtag() != null) {
builder.setEtag(tbResource.getEtag());

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/BaseResourceProcessor.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge.rpc.processor.resource;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
@ -87,7 +88,9 @@ public abstract class BaseResourceProcessor extends BaseEdgeProcessor {
resource.setResourceKey(resourceUpdateMsg.getResourceKey());
resource.setResourceType(ResourceType.valueOf(resourceUpdateMsg.getResourceType()));
resource.setFileName(resourceUpdateMsg.getFileName());
resource.setData(resourceUpdateMsg.hasData() ? resourceUpdateMsg.getData() : null);
if (resourceUpdateMsg.hasData()) {
resource.setEncodedData(resourceUpdateMsg.getData());
}
resource.setEtag(resourceUpdateMsg.hasEtag() ? resourceUpdateMsg.getEtag() : null);
return resource;
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java

@ -60,7 +60,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor {
return handleUnsupportedMsgType(resourceUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("files size limit is exhausted")) {
if (e.getMessage().contains("exceeds the maximum")) {
log.warn("[{}] Resource data size has been exhausted {}", tenantId, resourceUpdateMsg, e);
return Futures.immediateFuture(null);
} else {

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

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;

3
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.widgets.type;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -33,6 +35,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@AllArgsConstructor
public class DefaultWidgetTypeService extends AbstractTbEntityService implements TbWidgetTypeService {
private final WidgetTypeService widgetTypeService;
@Override

60
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -16,14 +16,15 @@
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
@ -44,6 +45,7 @@ import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.nio.file.DirectoryStream;
@ -51,12 +53,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource;
@ -109,6 +111,9 @@ public class InstallScripts {
@Autowired
private ResourceService resourceService;
@Autowired
private ImagesUpdater imagesUpdater;
Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
@ -153,16 +158,14 @@ public class InstallScripts {
}
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException {
findRuleChainsFromPath(ruleChainsPath)
.forEach(
path -> {
try {
createRuleChainFromFile(tenantId, path, null);
} catch (Exception e) {
log.error("Unable to load rule chain from json: [{}]", path.toString());
throw new RuntimeException("Unable to load rule chain from json", e);
}
});
findRuleChainsFromPath(ruleChainsPath).forEach(path -> {
try {
createRuleChainFromFile(tenantId, path, null);
} catch (Exception e) {
log.error("Unable to load rule chain from json: [{}]", path.toString());
throw new RuntimeException("Unable to load rule chain from json", e);
}
});
}
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException {
@ -195,6 +198,7 @@ public class InstallScripts {
}
public void loadSystemWidgets() throws Exception {
log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
@ -297,6 +301,30 @@ public class InstallScripts {
}
}
public void updateImages() {
imagesUpdater.updateWidgetsBundlesImages();
imagesUpdater.updateWidgetTypesImages();
imagesUpdater.updateDashboardsImages();
imagesUpdater.updateDeviceProfilesImages();
imagesUpdater.updateAssetProfilesImages();
}
@SneakyThrows
public void loadSystemImages() {
log.info("Loading system images...");
Stream<Path> dashboardsFiles = Files.list(Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR));
try (dashboardsFiles) {
dashboardsFiles.forEach(file -> {
try {
Dashboard dashboard = JacksonUtil.OBJECT_MAPPER.readValue(file.toFile(), Dashboard.class);
imagesUpdater.createSystemImages(dashboard);
} catch (Exception e) {
log.error("Failed to create system images for default dashboard {}", file.getFileName(), e);
}
});
}
}
public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception {
Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR);
loadDashboardsFromDir(tenantId, customerId, dashboardsDir);
@ -368,7 +396,7 @@ public class InstallScripts {
dirStream.forEach(
path -> {
try {
String data = Base64.getEncoder().encodeToString(Files.readAllBytes(path));
byte[] data = Files.readAllBytes(path);
TbResource tbResource = new TbResource();
tbResource.setTenantId(TenantId.SYS_TENANT_ID);
tbResource.setData(data);
@ -389,15 +417,13 @@ public class InstallScripts {
private void doSaveLwm2mResource(TbResource resource) throws ThingsboardException {
log.trace("Executing saveResource [{}]", resource);
if (StringUtils.isEmpty(resource.getData())) {
if (resource.getData() == null || resource.getData().length == 0) {
throw new DataValidationException("Resource data should be specified!");
}
toLwm2mResource(resource);
TbResource foundResource =
resourceService.getResource(TenantId.SYS_TENANT_ID, ResourceType.LWM2M_MODEL, resource.getResourceKey());
TbResource foundResource = resourceService.findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, ResourceType.LWM2M_MODEL, resource.getResourceKey());
if (foundResource == null) {
resourceService.saveResource(resource);
}
}
}

120
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -63,6 +63,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
@ -717,86 +718,73 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
}
break;
case "3.5.0":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, 3005000)) {
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.0", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005001;");
}
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
updateSchema("3.5.0", 3005000, "3.5.1", 3005001, null);
break;
case "3.5.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
if (isOldSchema(conn, 3005001)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.5.1", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package",
"asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"};
for (String entityName : entityNames) {
try {
conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE");
} catch (Exception e) {
}
}
updateSchema("3.5.1", 3005001, "3.6.0", 3006000, conn -> {
String[] entityNames = new String[]{"device", "component_descriptor", "customer", "dashboard", "rule_chain", "rule_node", "ota_package",
"asset_profile", "asset", "device_profile", "tb_user", "tenant_profile", "tenant", "widgets_bundle", "entity_view", "edge"};
for (String entityName : entityNames) {
try {
conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
conn.createStatement().execute("ALTER TABLE " + entityName + " DROP COLUMN " + SEARCH_TEXT + " CASCADE");
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
try {
conn.createStatement().execute("UPDATE rule_node SET " +
"configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3006000;");
log.info("Schema updated to version 3.6.0.");
} else {
log.info("Skip schema re-update to version 3.6.0. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.");
}
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
try {
conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
try {
conn.createStatement().execute("UPDATE rule_node SET " +
"configuration = (configuration::jsonb || '{\"updateAttributesOnlyOnValueChange\": \"false\"}'::jsonb)::varchar, " +
"configuration_version = 1 " +
"WHERE type = 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode' AND configuration_version < 1;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';");
} catch (Exception e) {
}
});
break;
case "3.6.0":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
if (isOldSchema(conn, 3006000)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.6.0", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3006001;");
log.info("Schema updated to version 3.6.1.");
} else {
log.info("Skip schema re-update to version 3.6.1. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.");
}
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
updateSchema("3.6.0", 3006000, "3.6.1", 3006001, null);
break;
case "3.6.1":
updateSchema("3.6.1", 3006001, "3.6.2", 3006002, null);
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
}
private void updateSchema(String oldVersionStr, int oldVersion, String newVersionStr, int newVersion, Consumer<Connection> additionalAction) {
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
if (isOldSchema(conn, oldVersion)) {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", oldVersionStr, SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
if (additionalAction != null) {
additionalAction.accept(conn);
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = " + newVersion + ";");
log.info("Schema updated to version {}", newVersionStr);
} else {
log.info("Skip schema re-update to version {}. Use env flag 'SKIP_SCHEMA_VERSION_CHECK' to force the re-update.", newVersionStr);
}
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
}
private void runSchemaUpdateScript(Connection connection, String version) throws Exception {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, connection);

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

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.HasImage;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
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;
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
public class ImagesUpdater {
private final ImageService imageService;
private final WidgetsBundleDao widgetsBundleDao;
private final WidgetTypeDao widgetTypeDao;
private final DashboardDao dashboardDao;
private final DeviceProfileDao deviceProfileDao;
private final AssetProfileDao assetProfileDao;
public void updateWidgetsBundlesImages() {
log.info("Updating widgets bundles images...");
var widgetsBundles = new PageDataIterable<>(widgetsBundleDao::findAllWidgetsBundles, 128);
updateImages(widgetsBundles, "bundle", imageService::replaceBase64WithImageUrl, widgetsBundleDao);
}
public void updateWidgetTypesImages() {
log.info("Updating widget types images...");
var widgetTypesIds = new PageDataIterable<>(widgetTypeDao::findAllWidgetTypesIds, 1024);
updateImages(widgetTypesIds, "widget type", imageService::replaceBase64WithImageUrl, widgetTypeDao);
}
public void updateDashboardsImages() {
log.info("Updating dashboards images...");
var dashboardsIds = new PageDataIterable<>(dashboardDao::findAllIds, 1024);
updateImages(dashboardsIds, "dashboard", imageService::replaceBase64WithImageUrl, dashboardDao);
}
public void createSystemImages(Dashboard defaultDashboard) {
defaultDashboard.setTenantId(TenantId.SYS_TENANT_ID);
boolean created = imageService.replaceBase64WithImageUrl(defaultDashboard);
if (created) {
log.debug("Created system images for default dashboard '{}'", defaultDashboard.getTitle());
}
}
public void updateDeviceProfilesImages() {
log.info("Updating device profiles images...");
var deviceProfiles = new PageDataIterable<>(deviceProfileDao::findAllWithImages, 256);
updateImages(deviceProfiles, "device profile", imageService::replaceBase64WithImageUrl, deviceProfileDao);
}
public void updateAssetProfilesImages() {
log.info("Updating asset profiles images...");
var assetProfiles = new PageDataIterable<>(assetProfileDao::findAllWithImages, 256);
updateImages(assetProfiles, "asset profile", imageService::replaceBase64WithImageUrl, assetProfileDao);
}
private <E extends HasImage> void updateImages(Iterable<E> entities, String type,
BiFunction<E, String, Boolean> updater, Dao<E> dao) {
int updatedCount = 0;
int totalCount = 0;
for (E entity : entities) {
totalCount++;
try {
boolean updated = updater.apply(entity, type);
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);
}
private <E extends HasImage> void updateImages(Iterable<? extends EntityId> entitiesIds, String type,
Function<E, Boolean> updater, Dao<E> 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);
}
}

16
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -83,6 +83,8 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.service.resource.TbImageService;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -141,6 +143,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
private final TbCoreConsumerStats stats;
protected final TbQueueConsumer<TbProtoQueueMsg<ToUsageStatsServiceMsg>> usageStatsConsumer;
private final TbQueueConsumer<TbProtoQueueMsg<ToOtaPackageStateServiceMsg>> firmwareStatesConsumer;
private final TbImageService imageService;
protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService usageStatsExecutor;
@ -166,7 +169,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
ApplicationEventPublisher eventPublisher,
Optional<JwtSettingsService> jwtSettingsService,
NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor) {
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, eventPublisher, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
@ -182,6 +186,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
this.vcQueueService = vcQueueService;
this.notificationSchedulerService = notificationSchedulerService;
this.notificationRuleProcessor = notificationRuleProcessor;
this.imageService = imageService;
}
@PostConstruct
@ -408,6 +413,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
.getNotificationRuleProcessorMsg().getTrigger().toByteArray());
notificationRuleTrigger.ifPresent(notificationRuleProcessor::process);
callback.onSuccess();
} else if (toCoreNotification.hasResourceCacheInvalidateMsg()) {
forwardToResourceService(toCoreNotification.getResourceCacheInvalidateMsg(), callback);
}
if (statsEnabled) {
stats.log(toCoreNotification);
@ -551,6 +558,13 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
callback.onSuccess();
}
private void forwardToResourceService(TransportProtos.ResourceCacheInvalidateMsg msg, TbCallback callback) {
var tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
imageService.evictETag(new ImageCacheKey(tenantId, msg.getResourceKey(), false));
imageService.evictETag(new ImageCacheKey(tenantId, msg.getResourceKey(), true));
callback.onSuccess();
}
private void forwardToSubMgrService(SubscriptionMgrMsgProto msg, TbCallback callback) {
if (msg.hasSubEvent()) {
TbEntitySubEventProto subEvent = msg.getSubEvent();

155
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbImageService.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.resource;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
@TbCoreComponent
public class DefaultTbImageService extends AbstractTbEntityService implements TbImageService {
private final TbClusterService clusterService;
private final ImageService imageService;
private final Cache<ImageCacheKey, String> etagCache;
public DefaultTbImageService(TbClusterService clusterService, ImageService imageService,
@Value("${cache.image.etag.timeToLiveInMinutes:44640}") int cacheTtl,
@Value("${cache.image.etag.maxSize:10000}") int cacheMaxSize) {
this.clusterService = clusterService;
this.imageService = imageService;
this.etagCache = Caffeine.newBuilder()
.expireAfterAccess(cacheTtl, TimeUnit.MINUTES)
.maximumSize(cacheMaxSize)
.build();
}
@Override
public String getETag(ImageCacheKey imageCacheKey) {
return etagCache.getIfPresent(imageCacheKey);
}
@Override
public void putETag(ImageCacheKey imageCacheKey, String etag) {
etagCache.put(imageCacheKey, etag);
}
@Override
public void evictETag(ImageCacheKey imageCacheKey) {
etagCache.invalidate(imageCacheKey);
}
@Override
public TbResourceInfo save(TbResource image, User user) throws Exception {
ActionType actionType = image.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = image.getTenantId();
try {
var oldEtag = getEtag(image);
if (image.getId() == null && StringUtils.isNotEmpty(image.getResourceKey())) {
var existingImage = imageService.getImageInfoByTenantIdAndKey(tenantId, image.getResourceKey());
if (existingImage != null) {
image.setId(existingImage.getId());
}
}
TbResourceInfo savedImage = imageService.saveImage(image);
notificationEntityService.logEntityAction(tenantId, savedImage.getId(), savedImage, actionType, user);
if (oldEtag.isPresent()) {
var newEtag = getEtag(savedImage);
if (newEtag.isPresent() && !oldEtag.get().equals(newEtag.get())) {
evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), false));
evictETag(new ImageCacheKey(image.getTenantId(), image.getResourceKey(), true));
clusterService.broadcastToCore(TransportProtos.ToCoreNotificationMsg.newBuilder()
.setResourceCacheInvalidateMsg(TransportProtos.ResourceCacheInvalidateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setResourceKey(image.getResourceKey())
.build())
.build());
}
}
return savedImage;
} catch (Exception e) {
image.setData(null);
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), image, actionType, user, e);
throw e;
}
}
private Optional<String> getEtag(TbResourceInfo image) throws JsonProcessingException {
var descriptor = image.getDescriptor(ImageDescriptor.class);
return Optional.ofNullable(descriptor != null ? descriptor.getEtag() : null);
}
private Optional<String> getPreviewEtag(TbResourceInfo image) throws JsonProcessingException {
var descriptor = image.getDescriptor(ImageDescriptor.class);
descriptor = descriptor != null ? descriptor.getPreviewDescriptor() : null;
return Optional.ofNullable(descriptor != null ? descriptor.getEtag() : null);
}
@Override
public TbResourceInfo save(TbResourceInfo imageInfo, User user) {
TenantId tenantId = imageInfo.getTenantId();
TbResourceId imageId = imageInfo.getId();
try {
imageInfo = imageService.saveImageInfo(imageInfo);
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user);
return imageInfo;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.UPDATED, user, e);
throw e;
}
}
@Override
public TbImageDeleteResult delete(TbResourceInfo imageInfo, User user, boolean force) {
TenantId tenantId = imageInfo.getTenantId();
TbResourceId imageId = imageInfo.getId();
try {
TbImageDeleteResult result = imageService.deleteImage(imageInfo, force);
if (result.isSuccess()) {
notificationEntityService.logEntityAction(tenantId, imageId, imageInfo, ActionType.DELETED, user, imageId.toString());
}
return result;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, imageId, ActionType.DELETED, user, e, imageId.toString());
throw e;
}
}
}

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

@ -15,34 +15,24 @@
*/
package org.thingsboard.server.service.resource;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@ -56,39 +46,51 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
@Slf4j
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService;
private final WidgetTypeService widgetTypeService;
public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) {
this.resourceService = resourceService;
this.widgetTypeService = widgetTypeService;
}
@Override
public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) {
return resourceService.getResource(tenantId, resourceType, resourceId);
}
@Override
public TbResource findResourceById(TenantId tenantId, TbResourceId resourceId) {
return resourceService.findResourceById(tenantId, resourceId);
}
@Override
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
return resourceService.findResourceInfoById(tenantId, resourceId);
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
public TbResource save(TbResource resource, User user) throws ThingsboardException {
if (resource.getResourceType() == ResourceType.IMAGE) {
throw new IllegalArgumentException("Image resource type is not supported");
}
ActionType actionType = resource.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = resource.getTenantId();
try {
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) {
toLwm2mResource(resource);
} else if (resource.getResourceKey() == null) {
resource.setResourceKey(resource.getFileName());
}
TbResource savedResource = resourceService.saveResource(resource);
tbClusterService.onResourceChange(savedResource, null);
notificationEntityService.logEntityAction(tenantId, savedResource.getId(), savedResource, actionType, user);
return savedResource;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
resource, actionType, user, e);
throw e;
}
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findTenantResourcesByTenantId(filter, pageLink);
public void delete(TbResource tbResource, User user) {
if (tbResource.getResourceType() == ResourceType.IMAGE) {
throw new IllegalArgumentException("Image resource type is not supported");
}
TbResourceId resourceId = tbResource.getId();
TenantId tenantId = tbResource.getTenantId();
try {
resourceService.deleteResource(tenantId, resourceId);
tbClusterService.onResourceDeleted(tbResource, null);
notificationEntityService.logEntityAction(tenantId, resourceId, tbResource, ActionType.DELETED, user, resourceId.toString());
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
ActionType.DELETED, user, e, resourceId.toString());
throw e;
}
}
@Override
@ -114,16 +116,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
.collect(Collectors.toList());
}
@Override
public void deleteResourcesByTenantId(TenantId tenantId) {
resourceService.deleteResourcesByTenantId(tenantId);
}
@Override
public long sumDataSizeByTenantId(TenantId tenantId) {
return resourceService.sumDataSizeByTenantId(tenantId);
}
private Comparator<? super LwM2mObject> getComparator(String sortProperty, String sortOrder) {
Comparator<LwM2mObject> comparator;
if ("name".equals(sortProperty)) {
@ -134,50 +126,4 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
return "DESC".equals(sortOrder) ? comparator.reversed() : comparator;
}
@Override
public TbResource save(TbResource tbResource, User user) throws ThingsboardException {
ActionType actionType = tbResource.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = tbResource.getTenantId();
try {
TbResource savedResource = checkNotNull(doSave(tbResource));
tbClusterService.onResourceChange(savedResource, null);
notificationEntityService.logEntityAction(tenantId, savedResource.getId(), savedResource, actionType, user);
return savedResource;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
tbResource, actionType, user, e);
throw e;
}
}
@Override
public void delete(TbResource tbResource, User user) {
TbResourceId resourceId = tbResource.getId();
TenantId tenantId = tbResource.getTenantId();
try {
resourceService.deleteResource(tenantId, resourceId);
tbClusterService.onResourceDeleted(tbResource, null);
notificationEntityService.logEntityAction(tenantId, resourceId, tbResource, ActionType.DELETED, user, resourceId.toString());
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),
ActionType.DELETED, user, e, resourceId.toString());
throw e;
}
}
private TbResource doSave(TbResource resource) throws ThingsboardException {
log.trace("Executing saveResource [{}]", resource);
if (StringUtils.isEmpty(resource.getData())) {
throw new DataValidationException("Resource data should be specified!");
}
if (ResourceType.LWM2M_MODEL.equals(resource.getResourceType())) {
toLwm2mResource(resource);
} else {
resource.setResourceKey(resource.getFileName());
}
HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes()));
resource.setEtag(hashCode.toString());
return resourceService.saveResource(resource);
}
}

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

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.dao.resource.ImageCacheKey;
public interface TbImageService {
TbResourceInfo save(TbResource image, User user) throws Exception;
TbResourceInfo save(TbResourceInfo imageInfo, User user);
TbImageDeleteResult delete(TbResourceInfo imageInfo, User user, boolean force);
String getETag(ImageCacheKey imageCacheKey);
void putETag(ImageCacheKey imageCacheKey, String etag);
void evictETag(ImageCacheKey imageCacheKey);
}

18
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -15,14 +15,9 @@
*/
package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
@ -30,16 +25,6 @@ import java.util.List;
public interface TbResourceService extends SimpleTbEntityService<TbResource> {
TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey);
TbResource findResourceById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List<LwM2mObject> findLwM2mObject(TenantId tenantId,
String sortOrder,
String sortProperty,
@ -50,7 +35,4 @@ public interface TbResourceService extends SimpleTbEntityService<TbResource> {
String sortOrder,
PageLink pageLink);
void deleteResourcesByTenantId(TenantId tenantId);
long sumDataSizeByTenantId(TenantId tenantId);
}

10
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java

@ -118,6 +118,16 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi
}
}
@Override
public <I extends EntityId> PageData<I> findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) {
ExportableEntityDao<I, ?> dao = getExportableEntityDao(entityType);
if (dao != null) {
return dao.findIdsByTenantId(tenantId.getId(), pageLink);
} else {
return new PageData<>();
}
}
@Override
public <I extends EntityId> I getExternalIdByInternal(I internalId) {
ExportableEntityDao<I, ?> dao = getExportableEntityDao(internalId.getEntityType());

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

@ -35,6 +35,8 @@ public interface ExportableEntitiesService {
<E extends ExportableEntity<I>, I extends EntityId> PageData<E> findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);
<I extends EntityId> PageData<I> findEntitiesIdsByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);
<I extends EntityId> I getExternalIdByInternal(I internalId);
<I extends EntityId> void removeById(TenantId tenantId, I id);

36
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/ResourceExportService.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Set;
@Service
@TbCoreComponent
public class ResourceExportService extends BaseEntityExportService<TbResourceId, TbResource, EntityExportData<TbResource>> {
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.TB_RESOURCE);
}
}

83
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class ResourceImportService extends BaseEntityImportService<TbResourceId, TbResource, EntityExportData<TbResource>> {
private final ResourceService resourceService;
@Override
protected void setOwner(TenantId tenantId, TbResource resource, IdProvider idProvider) {
resource.setTenantId(tenantId);
}
@Override
protected TbResource prepare(EntitiesImportCtx ctx, TbResource resource, TbResource oldResource, EntityExportData<TbResource> exportData, IdProvider idProvider) {
return resource;
}
@Override
protected TbResource findExistingEntity(EntitiesImportCtx ctx, TbResource resource, IdProvider idProvider) {
TbResource existingResource = super.findExistingEntity(ctx, resource, idProvider);
if (existingResource == null && ctx.isFindExistingByName()) {
existingResource = resourceService.findResourceByTenantIdAndKey(ctx.getTenantId(), resource.getResourceType(), resource.getResourceKey());
}
return existingResource;
}
@Override
protected boolean compare(EntitiesImportCtx ctx, EntityExportData<TbResource> exportData, TbResource prepared, TbResource existing) {
return true;
}
@Override
protected TbResource deepCopy(TbResource resource) {
return new TbResource(resource);
}
@Override
protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData<TbResource> exportData, IdProvider idProvider) {
return resourceService.saveResource(resource);
}
@Override
protected void onEntitySaved(User user, TbResource savedResource, TbResource oldResource) throws ThingsboardException {
super.onEntitySaved(user, savedResource, oldResource);
clusterService.onResourceChange(savedResource, null);
}
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;
}
}

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetTypeImportService.java

@ -20,13 +20,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.sync.ie.WidgetTypeExportData;
import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;

6
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -208,10 +208,10 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
if (config.isAllEntities()) {
DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink)
, 100, entity -> {
DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesIdsByTenantId(ctx.getTenantId(), entityType, pageLink),
100, entityId -> {
try {
ctx.add(saveEntityData(ctx, entity.getId()));
ctx.add(saveEntityData(ctx, entityId));
} catch (Exception e) {
throw new RuntimeException(e);
}

7
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -82,6 +82,7 @@ import org.thingsboard.server.dao.exception.EntitiesLimitException;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
@ -145,7 +146,7 @@ public class DefaultTransportApiService implements TransportApiService {
private final TbClusterService tbClusterService;
private final DataDecodingEncodingService dataDecodingEncodingService;
private final DeviceProvisionService deviceProvisionService;
private final TbResourceService resourceService;
private final ResourceService resourceService;
private final OtaPackageService otaPackageService;
private final OtaPackageDataCache otaPackageDataCache;
private final QueueService queueService;
@ -520,10 +521,10 @@ public class DefaultTransportApiService implements TransportApiService {
ResourceType resourceType = ResourceType.valueOf(requestMsg.getResourceType());
String resourceKey = requestMsg.getResourceKey();
TransportProtos.GetResourceResponseMsg.Builder builder = TransportProtos.GetResourceResponseMsg.newBuilder();
TbResource resource = resourceService.getResource(tenantId, resourceType, resourceKey);
TbResource resource = resourceService.findResourceByTenantIdAndKey(tenantId, resourceType, resourceKey);
if (resource == null && !tenantId.equals(TenantId.SYS_TENANT_ID)) {
resource = resourceService.getResource(TenantId.SYS_TENANT_ID, resourceType, resourceKey);
resource = resourceService.findResourceByTenantIdAndKey(TenantId.SYS_TENANT_ID, resourceType, resourceKey);
}
if (resource != null) {

10
application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java

@ -39,13 +39,13 @@ import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPA
@Slf4j
public class LwM2mObjectModelUtils {
private static final TbDDFFileParser ddfFileParser = new TbDDFFileParser();
public static void toLwm2mResource (TbResource resource) throws ThingsboardException {
public static void toLwm2mResource(TbResource resource) throws ThingsboardException {
try {
List<ObjectModel> objectModels =
ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText());
ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText());
if (!objectModels.isEmpty()) {
ObjectModel objectModel = objectModels.get(0);
@ -73,7 +73,7 @@ public class LwM2mObjectModelUtils {
public static LwM2mObject toLwM2mObject(TbResource resource, boolean isSave) {
try {
List<ObjectModel> objectModels =
ddfFileParser.parse(new ByteArrayInputStream(Base64.getDecoder().decode(resource.getData())), resource.getSearchText());
ddfFileParser.parse(new ByteArrayInputStream(resource.getData()), resource.getSearchText());
if (objectModels.size() == 0) {
return null;
} else {

6
application/src/main/resources/thingsboard.yml

@ -586,6 +586,12 @@ cache:
entityLimits:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL
maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled
image:
etag:
timeToLiveInMinutes: "${CACHE_SPECS_IMAGE_ETAGS_TTL:44640}" # Image ETags cache TTL
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
# Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required.

2
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -802,7 +802,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
}
protected <T> T doPut(String urlTemplate, T content, Class<T> responseClass, String... params) {
protected <T> T doPut(String urlTemplate, Object content, Class<T> responseClass, String... params) {
try {
return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) {

277
application/src/test/java/org/thingsboard/server/controller/ImageControllerTest.java

File diff suppressed because one or more lines are too long

118
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -48,6 +49,7 @@ import java.util.Base64;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -101,7 +103,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -116,9 +118,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(resource.getTitle(), savedResource.getTitle());
Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getFileName());
Assert.assertEquals(DEFAULT_FILE_NAME, savedResource.getResourceKey());
Assert.assertEquals(resource.getData(), savedResource.getData());
Assert.assertArrayEquals(resource.getData(), download(savedResource.getId()));
savedResource.setTitle("My new resource");
savedResource.setData(null);
save(savedResource);
@ -136,7 +139,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle(StringUtils.randomAlphabetic(300));
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
Mockito.reset(tbClusterService, auditLogService);
@ -155,7 +158,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -184,13 +187,14 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
TbResource foundResource = doGet("/api/resource/" + savedResource.getId().getId().toString(), TbResource.class);
TbResource foundResource = doGet("/api/resource/" + savedResource.getUuidId(), TbResource.class);
Assert.assertNotNull(foundResource);
Assert.assertEquals(savedResource, foundResource);
Assert.assertEquals(savedResource.getId(), foundResource.getId());
Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName());
}
@Test
@ -199,7 +203,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -213,7 +217,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.DELETED, ActionType.DELETED, resourceIdStr);
doGet("/api/resource/" + savedResource.getId().getId().toString())
doGet("/api/resource/" + savedResource.getUuidId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr))));
}
@ -224,7 +228,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -255,7 +259,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
resources.add(new TbResourceInfo(save(resource)));
}
List<TbResourceInfo> loadedResources = new ArrayList<>();
@ -292,7 +296,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("JKS Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
resources.add(new TbResourceInfo(save(resource)));
}
@ -302,7 +306,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("LWM2M Resource" + i);
resource.setResourceType(ResourceType.PKCS_12);
resource.setFileName(i + DEFAULT_FILE_NAME_2);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
save(resource);
}
@ -339,7 +343,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
resources.add(new TbResourceInfo(save(resource)));
}
List<TbResourceInfo> loadedResources = new ArrayList<>();
@ -399,7 +403,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("JKS Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResourceInfo saved = new TbResourceInfo(save(resource));
jksResources.add(saved);
}
@ -410,7 +414,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("LWM2M Resource" + i);
resource.setResourceType(ResourceType.PKCS_12);
resource.setFileName(i + DEFAULT_FILE_NAME_2);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource saved = save(resource);
lwm2mesources.add(saved);
}
@ -476,7 +480,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
expectedResources.add(new TbResourceInfo(save(resource)));
}
@ -487,7 +491,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setTitle("Resource" + i);
resource.setResourceType(ResourceType.JKS);
resource.setFileName(i + DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResourceInfo savedResource = new TbResourceInfo(save(resource));
systemResources.add(savedResource);
if (i >= 73) {
@ -531,7 +535,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("Js resource");
resource.setFileName(JS_TEST_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -562,7 +566,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("Js resource");
resource.setFileName(JS_TEST_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -606,7 +610,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("Js resource");
resource.setFileName(JS_TEST_FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
@ -616,6 +620,52 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(status().isForbidden());
}
@Test
public void testUpdateResourceData_nonUpdatableResourceType() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.PKCS_12);
resource.setTitle("My resource");
resource.setFileName("3.pks");
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
resource.setEtag(savedResource.getEtag());
savedResource.setEncodedData(TEST_DATA);
doPost("/api/resource", savedResource)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("can't be updated")));
savedResource.setData(null);
savedResource.setTitle("Updated resource");
savedResource = doPost("/api/resource", savedResource, TbResource.class);
assertThat(savedResource.getTitle()).isEqualTo("Updated resource");
assertThat(savedResource.getFileName()).isEqualTo(resource.getFileName());
assertThat(savedResource.getEtag()).isEqualTo(resource.getEtag());
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(TEST_DATA);
}
@Test
public void testUpdateResourceData_updatableResourceType() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("My resource");
resource.setFileName("module.js");
resource.setEncodedData(TEST_DATA);
TbResource savedResource = save(resource);
resource.setEtag(savedResource.getEtag());
String newData = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3});
savedResource.setEncodedData(newData);
savedResource.setFileName("new-module.js");
savedResource.setTitle("Updated title");
savedResource = save(savedResource);
assertThat(savedResource.getTitle()).isEqualTo("Updated title");
assertThat(savedResource.getFileName()).isEqualTo("new-module.js");
assertThat(savedResource.getEtag()).isNotEqualTo(resource.getEtag());
assertThat(download(savedResource.getId())).asBase64Encoded().isEqualTo(newData);
}
@Test
public void testGetLwm2mListObjectsPage() throws Exception {
loginTenantAdmin();
@ -623,7 +673,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
List<TbResource> resources = loadLwm2mResources();
List<LwM2mObject> objects =
doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>(){});
doGetTyped("/api/resource/lwm2m/page?pageSize=100&page=0", new TypeReference<>() {});
Assert.assertNotNull(objects);
Assert.assertEquals(resources.size(), objects.size());
@ -637,13 +687,29 @@ public class TbResourceControllerTest extends AbstractControllerTest {
List<TbResource> resources = loadLwm2mResources();
List<LwM2mObject> objects =
doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>(){});
doGetTyped("/api/resource/lwm2m?sortProperty=id&sortOrder=ASC&objectIds=3_1.0,5_1.0,19_1.1", new TypeReference<>() {});
Assert.assertNotNull(objects);
Assert.assertEquals(3, objects.size());
removeLoadResources(resources);
}
private TbResource save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
});
}
private byte[] download(TbResourceId resourceId) throws Exception {
return doGet("/api/resource/" + resourceId + "/download")
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsByteArray();
}
private String getImageLink(TbResourceInfo resourceInfo) {
return "/api/images/" + (resourceInfo.getTenantId().isSysTenantId() ? "system/" : "") + resourceInfo.getResourceKey();
}
private List<TbResource> loadLwm2mResources() throws Exception {
var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303");
@ -656,7 +722,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.LWM2M_MODEL);
resource.setFileName(fileName);
resource.setData(Base64.getEncoder().encodeToString(bytes));
resource.setData(bytes);
resources.add(save(resource));
}
@ -670,8 +736,4 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}
}
private TbResource save(TbResource tbResource) throws Exception {
return doPostWithTypedResponse("/api/resource", tbResource, new TypeReference<>() {
});
}
}

7
application/src/test/java/org/thingsboard/server/edge/ResourceEdgeTest.java

@ -47,7 +47,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
resource.setResourceType(ResourceType.JKS);
resource.setTitle("Edge Test Resource");
resource.setFileName(FILE_NAME);
resource.setData(TEST_DATA);
resource.setEncodedData(TEST_DATA);
edgeImitator.expectMessageAmount(1);
TbResource savedResource = doPost("/api/resource", resource, TbResource.class);
@ -65,7 +65,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(ResourceType.JKS, tbResource.getResourceType());
Assert.assertEquals(FILE_NAME, tbResource.getResourceKey());
Assert.assertEquals(FILE_NAME, tbResource.getFileName());
Assert.assertEquals(TEST_DATA, tbResource.getData());
Assert.assertEquals(TEST_DATA, tbResource.getEncodedData());
Assert.assertTrue(StringUtils.isNotBlank(tbResource.getEtag()));
// update resource
@ -121,6 +121,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
TbResource tb = doGet("/api/resource/" + uuid, TbResource.class);
Assert.assertNotNull(tb);
Assert.assertEquals("Edge Test Resource", tb.getName());
Assert.assertEquals(TEST_DATA, tb.getEncodedData());
}
@Test
@ -171,7 +172,7 @@ public class ResourceEdgeTest extends AbstractEdgeTest {
tbResource.setResourceType(ResourceType.JKS);
tbResource.setResourceKey(FILE_NAME);
tbResource.setFileName(FILE_NAME);
tbResource.setData(TEST_DATA);
tbResource.setEncodedData(TEST_DATA);
return tbResource;
}
}

3
application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
import java.io.IOException;
import java.nio.file.Path;
@ -63,6 +64,8 @@ class InstallScriptsTest {
OAuth2ConfigTemplateService oAuth2TemplateService;
@MockBean
ResourceService resourceService;
@MockBean
ImagesUpdater imagesUpdater;
@SpyBean
InstallScripts installScripts;

90
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

File diff suppressed because one or more lines are too long

4
application/src/test/java/org/thingsboard/server/service/transport/DefaultTransportApiServiceTest.java

@ -44,13 +44,13 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import java.io.IOException;
import java.nio.file.Files;
@ -96,7 +96,7 @@ public class DefaultTransportApiServiceTest {
@MockBean
protected DeviceProvisionService deviceProvisionService;
@MockBean
protected TbResourceService resourceService;
protected ResourceService resourceService;
@MockBean
protected OtaPackageService otaPackageService;
@MockBean

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -206,7 +206,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
lwModel.setFileName(resourceName);
lwModel.setTenantId(tenantId);
byte[] bytes = IOUtils.toByteArray(AbstractLwM2MIntegrationTest.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName));
lwModel.setData(Base64.getEncoder().encodeToString(bytes));
lwModel.setData(bytes);
lwModel = doPostWithTypedResponse("/api/resource", lwModel, new TypeReference<>() {
});
Assert.assertNotNull(lwModel);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java

@ -29,7 +29,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.List;
public interface DashboardService extends EntityDaoService {
Dashboard findDashboardById(TenantId tenantId, DashboardId dashboardId);
ListenableFuture<Dashboard> findDashboardByIdAsync(TenantId tenantId, DashboardId dashboardId);

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

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
public interface ImageService {
TbResourceInfo saveImage(TbResource image);
TbResourceInfo saveImageInfo(TbResourceInfo imageInfo);
TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key);
PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink);
byte[] getImageData(TenantId tenantId, TbResourceId imageId);
byte[] getImagePreview(TenantId tenantId, TbResourceId imageId);
TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force);
TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag);
boolean replaceBase64WithImageUrl(HasImage entity, String type);
boolean replaceBase64WithImageUrl(Dashboard dashboard);
boolean replaceBase64WithImageUrl(WidgetTypeDetails widgetType);
void inlineImage(HasImage entity);
void inlineImages(Dashboard dashboard);
void inlineImages(WidgetTypeDetails widgetTypeDetails);
}

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

@ -34,12 +34,14 @@ public interface ResourceService extends EntityDaoService {
TbResource saveResource(TbResource resource, boolean doValidate);
TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId);
TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
TbResource findResourceById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
PageData<TbResource> findAllTenantResources(TenantId tenantId, PageLink pageLink);
ListenableFuture<TbResourceInfo> findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId);
@ -54,7 +56,10 @@ public interface ResourceService extends EntityDaoService {
void deleteResource(TenantId tenantId, TbResourceId resourceId);
void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force);
void deleteResourcesByTenantId(TenantId tenantId);
long sumDataSizeByTenantId(TenantId tenantId);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -30,7 +30,7 @@ import java.util.Objects;
import java.util.Set;
@ApiModel
public class DashboardInfo extends BaseData<DashboardId> implements HasName, HasTenantId, HasTitle {
public class DashboardInfo extends BaseData<DashboardId> implements HasName, HasTenantId, HasTitle, HasImage {
private static final long serialVersionUID = -9080404114760433799L;

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -50,6 +50,7 @@ public class DataConstants {
public static final String HTTP_TRANSPORT_NAME = "HTTP";
public static final String SNMP_TRANSPORT_NAME = "SNMP";
public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!";
public static final String TB_IMAGE_PREFIX = "tb-image;";
public static final String[] allScopes() {

2
common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java

@ -41,7 +41,7 @@ import java.io.IOException;
@ToString(exclude = {"image", "profileDataBytes"})
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity<DeviceProfileId> {
public class DeviceProfile extends BaseData<DeviceProfileId> implements HasName, HasTenantId, HasOtaPackage, HasRuleEngineProfile, ExportableEntity<DeviceProfileId>, HasImage {
private static final long serialVersionUID = 6998485460273302018L;

24
common/data/src/main/java/org/thingsboard/server/common/data/HasImage.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
public interface HasImage extends HasTenantId, HasName {
String getImage();
void setImage(String image);
}

30
common/data/src/main/java/org/thingsboard/server/common/data/ImageDescriptor.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ImageDescriptor {
private String mediaType;
private int width;
private int height;
private long size;
private String etag;
private ImageDescriptor previewDescriptor;
}

33
common/data/src/main/java/org/thingsboard/server/common/data/ImageExportData.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@ApiModel
@Slf4j
@Data
public class ImageExportData {
private final String mediaType;
private final String fileName;
private final String title;
private final String resourceKey;
private final String data;
}

17
common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java

@ -16,20 +16,21 @@
package org.thingsboard.server.common.data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum ResourceType {
LWM2M_MODEL("application/xml", false),
JKS("application/x-java-keystore", false),
PKCS_12("application/x-pkcs12", false),
JS_MODULE("application/javascript", true);
LWM2M_MODEL("application/xml", false, false),
JKS("application/x-java-keystore", false, false),
PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true);
@Getter
private final String mediaType;
@Getter
private final boolean customerAccess;
@Getter
private final boolean updatable;
ResourceType(String mediaType, boolean customerAccess) {
this.mediaType = mediaType;
this.customerAccess = customerAccess;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -173,6 +173,10 @@ public class StringUtils {
return false;
}
public static String substringBeforeLast(String str, String separator) {
return org.apache.commons.lang3.StringUtils.substringBeforeLast(str, separator);
}
public static String substringAfterLast(String str, String sep) {
return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep);
}

32
common/data/src/main/java/org/thingsboard/server/common/data/TbImageDeleteResult.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.HasId;
import java.util.List;
import java.util.Map;
@Data
@Builder
public class TbImageDeleteResult {
private boolean success;
private Map<String, List<? extends HasId<?>>> references;
}

69
common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java

File diff suppressed because one or more lines are too long

77
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -16,6 +16,9 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -26,11 +29,13 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.function.UnaryOperator;
@ApiModel
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, HasTenantId {
public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, HasTenantId, ExportableEntity<TbResourceId> {
private static final long serialVersionUID = 7282664529021651736L;
@ -48,8 +53,16 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
private String resourceKey;
@ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String searchText;
@ApiModelProperty(position = 8, value = "Resource etag.", example = "33a64df551425fcc55e4d42a148795d9f25f89d4", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String etag;
@NoXss
@Length(fieldName = "file name")
@ApiModelProperty(position = 9, value = "Resource file name.", example = "19.xml", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String fileName;
private JsonNode descriptor;
private TbResourceId externalId;
public TbResourceInfo() {
super();
@ -61,18 +74,21 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
public TbResourceInfo(TbResourceInfo resourceInfo) {
super(resourceInfo);
this.tenantId = resourceInfo.getTenantId();
this.title = resourceInfo.getTitle();
this.resourceType = resourceInfo.getResourceType();
this.resourceKey = resourceInfo.getResourceKey();
this.searchText = resourceInfo.getSearchText();
this.etag = resourceInfo.getEtag();
this.tenantId = resourceInfo.tenantId;
this.title = resourceInfo.title;
this.resourceType = resourceInfo.resourceType;
this.resourceKey = resourceInfo.resourceKey;
this.searchText = resourceInfo.searchText;
this.etag = resourceInfo.etag;
this.fileName = resourceInfo.fileName;
this.descriptor = resourceInfo.descriptor != null ? resourceInfo.descriptor.deepCopy() : null;
this.externalId = resourceInfo.externalId;
}
@ApiModelProperty(position = 1, value = "JSON object with the Resource Id. " +
"Specify this field to update the Resource. " +
"Referencing non-existing Resource Id will cause error. " +
"Omit this field to create new Resource." )
"Omit this field to create new Resource.")
@Override
public TbResourceId getId() {
return super.getId();
@ -85,34 +101,37 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
}
@Override
@JsonIgnore
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return title;
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getLink() {
if (resourceType == ResourceType.IMAGE) {
return "/api/images/" + ((tenantId == null || !tenantId.isSysTenantId()) ? "tenant" : "system") + "/" + resourceKey; // tenantId is null in case of export to git
}
return null;
}
@JsonIgnore
public String getSearchText() {
return searchText != null ? searchText : title;
return title;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ResourceInfo [tenantId=");
builder.append(tenantId);
builder.append(", id=");
builder.append(getUuidId());
builder.append(", createdTime=");
builder.append(createdTime);
builder.append(", title=");
builder.append(title);
builder.append(", resourceType=");
builder.append(resourceType);
builder.append(", resourceKey=");
builder.append(resourceKey);
builder.append(", hashCode=");
builder.append(etag);
builder.append("]");
return builder.toString();
public <T> T getDescriptor(Class<T> type) throws JsonProcessingException {
return descriptor != null ? mapper.treeToValue(descriptor, type) : null;
}
public <T> void updateDescriptor(Class<T> type, UnaryOperator<T> updater) throws JsonProcessingException {
T descriptor = getDescriptor(type);
descriptor = updater.apply(descriptor);
setDescriptorValue(descriptor);
}
@JsonIgnore
public void setDescriptorValue(Object value) {
this.descriptor = value != null ? mapper.valueToTree(value) : null;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java

@ -19,11 +19,13 @@ import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Set;
@Data
@Builder
public class TbResourceInfoFilter {
private TenantId tenantId;
private ResourceType resourceType;
private Set<ResourceType> resourceTypes;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java

@ -22,8 +22,8 @@ import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.HasTenantId;
@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ToString(exclude = {"image"})
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class AssetProfile extends BaseData<AssetProfileId> implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity<AssetProfileId> {
public class AssetProfile extends BaseData<AssetProfileId> implements HasName, HasTenantId, HasRuleEngineProfile, ExportableEntity<AssetProfileId>, HasImage {
private static final long serialVersionUID = 6998485460273302018L;
@ -114,7 +114,7 @@ public class AssetProfile extends BaseData<AssetProfileId> implements HasName, H
}
@ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the asset profile is not specified during asset creation.")
public boolean isDefault(){
public boolean isDefault() {
return isDefault;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
@ -56,7 +57,8 @@ import java.lang.annotation.Target;
@Type(name = "WIDGET_TYPE", value = WidgetTypeDetails.class),
@Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class),
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class)
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class),
@Type(name = "TB_RESOURCE", value = TbResource.class)
})
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
public @interface JsonTbEntity {

1
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -39,6 +39,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxRuleChains;
private long maxResourcesInBytes;
private long maxOtaPackagesInBytes;
private long maxResourceSize;
private String transportTenantMsgRateLimit;
private String transportTenantTelemetryMsgRateLimit;

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

@ -21,6 +21,7 @@ import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@ -29,10 +30,10 @@ import org.thingsboard.server.common.data.validation.NoXss;
@Data
@JsonPropertyOrder({ "fqn", "name", "deprecated", "image", "description", "descriptor", "externalId" })
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, ExportableEntity<WidgetTypeId> {
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> {
@Length(fieldName = "image", max = 1000000)
@ApiModelProperty(position = 9, value = "Base64 encoded thumbnail")
@ApiModelProperty(position = 9, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")
private String image;
@NoXss
@Length(fieldName = "description", max = 1024)

7
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java

@ -22,8 +22,8 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasTitle;
@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel
@EqualsAndHashCode(callSuper = true)
public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName, HasTenantId, ExportableEntity<WidgetsBundleId>, HasTitle {
public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName, HasTenantId, ExportableEntity<WidgetsBundleId>, HasTitle, HasImage {
private static final long serialVersionUID = -7627368878362410489L;
@ -60,12 +60,13 @@ public class WidgetsBundle extends BaseData<WidgetsBundleId> implements HasName,
@Length(fieldName = "image", max = 1000000)
@Getter
@Setter
@ApiModelProperty(position = 6, value = "Base64 encoded thumbnail", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@ApiModelProperty(position = 6, value = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String image;
@NoXss
@Length(fieldName = "description", max = 1024)
@Getter
@Setter
@ApiModelProperty(position = 7, value = "Description", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String description;

7
common/proto/src/main/proto/queue.proto

@ -306,6 +306,12 @@ message CoreStartupMsg {
int64 ts = 3;
}
message ResourceCacheInvalidateMsg {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
string resourceKey = 3;
}
message LwM2MRegistrationRequestMsg {
string tenantId = 1;
string endpoint = 2;
@ -1281,6 +1287,7 @@ message ToCoreNotificationMsg {
EdgeEventUpdateMsgProto edgeEventUpdate = 14;
ToEdgeSyncRequestMsgProto toEdgeSyncRequest = 15;
FromEdgeSyncResponseMsgProto fromEdgeSyncResponse = 16;
ResourceCacheInvalidateMsg resourceCacheInvalidateMsg = 17;
}
/* Messages that are handled by ThingsBoard RuleEngine Service */

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java

@ -152,8 +152,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
private ObjectModel getObjectModel(String key) {
Optional<TbResource> tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key);
return tbResource.map(resource -> helper.parseFromXmlToObjectModel(
Base64.getDecoder().decode(resource.getData()),
return tbResource.map(resource -> helper.parseFromXmlToObjectModel(resource.getData(),
key + ".xml")).orElse(null);
}
}

11
common/util/src/main/java/org/thingsboard/common/util/RegexUtils.java

@ -19,6 +19,7 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -37,4 +38,14 @@ public class RegexUtils {
return pattern.matcher(input).matches();
}
public static String getMatch(String input, Pattern pattern, int group) {
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
try {
return matcher.group(group);
} catch (Exception ignored) {}
}
return null;
}
}

12
dao/pom.xml

@ -234,6 +234,18 @@
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
</dependency>
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

6
dao/src/main/java/org/thingsboard/server/dao/ExportableEntityDao.java

@ -22,7 +22,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import java.util.UUID;
public interface ExportableEntityDao<I extends EntityId, T extends ExportableEntity<?>> extends Dao<T> {
public interface ExportableEntityDao<I extends EntityId, T extends ExportableEntity<I>> extends Dao<T> {
T findByTenantIdAndExternalId(UUID tenantId, UUID externalId);
@ -30,6 +30,10 @@ public interface ExportableEntityDao<I extends EntityId, T extends ExportableEnt
PageData<T> findByTenantId(UUID tenantId, PageLink pageLink);
default PageData<I> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return findByTenantId(tenantId, pageLink).mapData(ExportableEntity::getId);
}
I getExternalIdByInternal(I internalId);
}

29
dao/src/main/java/org/thingsboard/server/dao/ImageContainerDao.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.List;
public interface ImageContainerDao<T extends HasId<?>> {
List<T> findByTenantAndImageLink(TenantId tenantId, String imageUrl, int limit);
List<T> findByImageLink(String imageUrl, int limit);
}

2
dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java

@ -24,8 +24,10 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.TenantEntityDao;
import java.util.List;

6
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java

@ -23,10 +23,11 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ImageContainerDao;
import java.util.UUID;
public interface AssetProfileDao extends Dao<AssetProfile>, ExportableEntityDao<AssetProfileId, AssetProfile> {
public interface AssetProfileDao extends Dao<AssetProfile>, ExportableEntityDao<AssetProfileId, AssetProfile>, ImageContainerDao<AssetProfileInfo> {
AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, UUID assetProfileId);
@ -43,4 +44,7 @@ public interface AssetProfileDao extends Dao<AssetProfile>, ExportableEntityDao<
AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId);
AssetProfile findByName(TenantId tenantId, String profileName);
PageData<AssetProfile> findAllWithImages(PageLink pageLink);
}

5
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -71,6 +72,9 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService<AssetPr
@Autowired
private DataValidator<AssetProfile> assetProfileValidator;
@Autowired
private ImageService imageService;
@TransactionalEventListener(classes = AssetProfileEvictEvent.class)
@Override
public void handleEvictEvent(AssetProfileEvictEvent event) {
@ -141,6 +145,7 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService<AssetPr
}
AssetProfile savedAssetProfile;
try {
imageService.replaceBase64WithImageUrl(assetProfile, "asset profile");
savedAssetProfile = assetProfileDao.saveAndFlush(assetProfile.getTenantId(), assetProfile);
publishEvictEvent(new AssetProfileEvictEvent(savedAssetProfile.getTenantId(), savedAssetProfile.getName(),
oldAssetProfile != null ? oldAssetProfile.getName() : null, savedAssetProfile.getId(), savedAssetProfile.isDefault()));

2
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java

@ -44,4 +44,6 @@ public interface DashboardDao extends Dao<Dashboard>, TenantEntityDao, Exportabl
PageData<DashboardId> findIdsByTenantId(TenantId tenantId, PageLink pageLink);
PageData<DashboardId> findAllIds(PageLink pageLink);
}

7
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java

@ -16,16 +16,20 @@
package org.thingsboard.server.dao.dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ImageContainerDao;
import java.util.List;
import java.util.UUID;
/**
* The Interface DashboardInfoDao.
*/
public interface DashboardInfoDao extends Dao<DashboardInfo> {
public interface DashboardInfoDao extends Dao<DashboardInfo>, ImageContainerDao<DashboardInfo> {
/**
* Find dashboards by tenantId and page link.
@ -78,4 +82,5 @@ public interface DashboardInfoDao extends Dao<DashboardInfo> {
DashboardInfo findFirstByTenantIdAndName(UUID tenantId, String name);
String findTitleById(UUID tenantId, UUID dashboardId);
}

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

@ -50,6 +50,7 @@ import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -78,6 +79,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Autowired
private EdgeDao edgeDao;
@Autowired
private ImageService imageService;
@Autowired
private DataValidator<Dashboard> dashboardValidator;
@ -153,6 +157,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
dashboardValidator.validate(dashboard, DashboardInfo::getTenantId);
}
try {
imageService.replaceBase64WithImageUrl(dashboard);
var saved = dashboardDao.save(dashboard.getTenantId(), dashboard);
publishEvictEvent(new DashboardTitleEvictEvent(saved.getId()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId())
@ -170,6 +175,8 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
}
}
@Override
public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId) {
Dashboard dashboard = findDashboardById(tenantId, dashboardId);

6
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java

@ -23,10 +23,11 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ImageContainerDao;
import java.util.UUID;
public interface DeviceProfileDao extends Dao<DeviceProfile>, ExportableEntityDao<DeviceProfileId, DeviceProfile> {
public interface DeviceProfileDao extends Dao<DeviceProfile>, ExportableEntityDao<DeviceProfileId, DeviceProfile>, ImageContainerDao<DeviceProfileInfo> {
DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, UUID deviceProfileId);
@ -45,4 +46,7 @@ public interface DeviceProfileDao extends Dao<DeviceProfile>, ExportableEntityDa
DeviceProfile findByProvisionDeviceKey(String provisionDeviceKey);
DeviceProfile findByName(TenantId tenantId, String profileName);
PageData<DeviceProfile> findAllWithImages(PageLink pageLink);
}

5
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -47,6 +47,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
@ -91,6 +92,9 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
@Autowired
private QueueService queueService;
@Autowired
private ImageService imageService;
@TransactionalEventListener(classes = DeviceProfileEvictEvent.class)
@Override
public void handleEvictEvent(DeviceProfileEvictEvent event) {
@ -178,6 +182,7 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService<Device
}
DeviceProfile savedDeviceProfile;
try {
imageService.replaceBase64WithImageUrl(deviceProfile, "device profile");
savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile);
publishEvictEvent(new DeviceProfileEvictEvent(savedDeviceProfile.getTenantId(), savedDeviceProfile.getName(),
oldDeviceProfile != null ? oldDeviceProfile.getName() : null, savedDeviceProfile.getId(), savedDeviceProfile.isDefault(),

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -499,6 +499,8 @@ public class ModelConstants {
public static final String RESOURCE_FILE_NAME_COLUMN = "file_name";
public static final String RESOURCE_DATA_COLUMN = "data";
public static final String RESOURCE_ETAG_COLUMN = "etag";
public static final String RESOURCE_DESCRIPTOR_COLUMN = "descriptor";
public static final String RESOURCE_PREVIEW_COLUMN = "preview";
/**
* Ota Package constants.

31
dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java

@ -15,24 +15,30 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_PREVIEW_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TITLE_COLUMN;
@ -42,8 +48,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = RESOURCE_TABLE_NAME)
public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseEntity<TbResource> {
public class TbResourceEntity extends BaseSqlEntity<TbResource> {
@Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid")
private UUID tenantId;
@ -64,11 +71,21 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
private String fileName;
@Column(name = RESOURCE_DATA_COLUMN)
private String data;
private byte[] data;
@Column(name = RESOURCE_ETAG_COLUMN)
private String etag;
@Type(type = "json")
@Column(name = RESOURCE_DESCRIPTOR_COLUMN)
private JsonNode descriptor;
@Column(name = RESOURCE_PREVIEW_COLUMN)
private byte[] preview;
@Column(name = EXTERNAL_ID_PROPERTY)
private UUID externalId;
public TbResourceEntity() {
}
@ -87,6 +104,9 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
this.fileName = resource.getFileName();
this.data = resource.getData();
this.etag = resource.getEtag();
this.descriptor = resource.getDescriptor();
this.preview = resource.getPreview();
this.externalId = getUuid(resource.getExternalId());
}
@Override
@ -101,6 +121,9 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
resource.setFileName(fileName);
resource.setData(data);
resource.setEtag(etag);
resource.setDescriptor(descriptor);
resource.setPreview(preview);
resource.setExternalId(getEntityId(externalId, TbResourceId::new));
return resource;
}

30
dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java

@ -15,21 +15,28 @@
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DESCRIPTOR_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ETAG_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
@ -40,6 +47,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPER
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = RESOURCE_TABLE_NAME)
public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implements BaseEntity<TbResourceInfo> {
@ -59,7 +67,17 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
private String searchText;
@Column(name = RESOURCE_ETAG_COLUMN)
private String hashCode;
private String etag;
@Column(name = RESOURCE_FILE_NAME_COLUMN)
private String fileName;
@Type(type = "json")
@Column(name = RESOURCE_DESCRIPTOR_COLUMN)
private JsonNode descriptor;
@Column(name = EXTERNAL_ID_PROPERTY)
private UUID externalId;
public TbResourceInfoEntity() {
}
@ -74,7 +92,10 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
this.resourceType = resource.getResourceType().name();
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.hashCode = resource.getEtag();
this.etag = resource.getEtag();
this.fileName = resource.getFileName();
this.descriptor = resource.getDescriptor();
this.externalId = getUuid(resource.getExternalId());
}
@Override
@ -86,7 +107,10 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
resource.setResourceType(ResourceType.valueOf(resourceType));
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setEtag(hashCode);
resource.setEtag(etag);
resource.setFileName(fileName);
resource.setDescriptor(descriptor);
resource.setExternalId(getEntityId(externalId, TbResourceId::new));
return resource;
}
}

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

@ -0,0 +1,615 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.resource;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Base64Utils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasImage;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
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 org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.dashboard.DashboardInfoDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.util.ImageUtils.ProcessedImage;
import org.thingsboard.server.dao.util.JsonNodeProcessingTask;
import org.thingsboard.server.dao.util.JsonPathProcessingTask;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Pattern;
@Service
@Slf4j
public class BaseImageService extends BaseResourceService implements ImageService {
private static final int MAX_ENTITIES_TO_FIND = 10;
public static Map<String, String> DASHBOARD_BASE64_MAPPING = new HashMap<>();
public static Map<String, String> WIDGET_TYPE_BASE64_MAPPING = new HashMap<>();
static {
DASHBOARD_BASE64_MAPPING.put("settings.dashboardLogoUrl", "$prefix logo");
DASHBOARD_BASE64_MAPPING.put("states.default.layouts.main.gridSettings.backgroundImageUrl", "$prefix background");
DASHBOARD_BASE64_MAPPING.put("states.default.layouts.right.gridSettings.backgroundImageUrl", "$prefix right background");
DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.main.gridSettings.backgroundImageUrl", "$prefix $stateId background");
DASHBOARD_BASE64_MAPPING.put("states.$stateId.layouts.right.gridSettings.backgroundImageUrl", "$prefix $stateId right background");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.backgroundImageUrl", "$prefix widget \"$title\" background");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.mapImageUrl", "$prefix widget \"$title\" map image");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImage", "$prefix widget \"$title\" marker image");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.markerImages", "$prefix widget \"$title\" marker image $index");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageUrl", "$prefix widget \"$title\" background");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].settings.background.imageBase64", "$prefix widget \"$title\" background");
DASHBOARD_BASE64_MAPPING.put("widgets.*.config[$title].datasources.*.dataKeys.*.settings.customIcon", "$prefix widget \"$title\" custom icon");
WIDGET_TYPE_BASE64_MAPPING.put("settings.backgroundImageUrl", "$prefix background");
WIDGET_TYPE_BASE64_MAPPING.put("settings.mapImageUrl", "$prefix map image");
WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImage", "Map marker image");
WIDGET_TYPE_BASE64_MAPPING.put("settings.markerImages", "Map marker image $index");
WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageUrl", "$prefix background");
WIDGET_TYPE_BASE64_MAPPING.put("settings.background.imageBase64", "$prefix background");
WIDGET_TYPE_BASE64_MAPPING.put("datasources.*.dataKeys.*.settings.customIcon", "$prefix custom icon");
}
private final AssetProfileDao assetProfileDao;
private final DeviceProfileDao deviceProfileDao;
private final WidgetsBundleDao widgetsBundleDao;
private final WidgetTypeDao widgetTypeDao;
private final DashboardInfoDao dashboardInfoDao;
private final Map<EntityType, ImageContainerDao<?>> imageContainerDaoMap = new HashMap<>();
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator,
AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao,
WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) {
super(resourceDao, resourceInfoDao, resourceValidator);
this.assetProfileDao = assetProfileDao;
this.deviceProfileDao = deviceProfileDao;
this.widgetsBundleDao = widgetsBundleDao;
this.widgetTypeDao = widgetTypeDao;
this.dashboardInfoDao = dashboardInfoDao;
}
@PostConstruct
public void init() {
imageContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
imageContainerDaoMap.put(EntityType.WIDGETS_BUNDLE, widgetsBundleDao);
imageContainerDaoMap.put(EntityType.DEVICE_PROFILE, deviceProfileDao);
imageContainerDaoMap.put(EntityType.ASSET_PROFILE, assetProfileDao);
imageContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
}
@Override
@SneakyThrows
public TbResourceInfo saveImage(TbResource image) {
if (image.getId() == null && StringUtils.isEmpty(image.getResourceKey())) {
image.setResourceKey(getUniqueKey(image.getTenantId(), image.getFileName()));
}
resourceValidator.validate(image, TbResourceInfo::getTenantId);
ImageDescriptor descriptor = image.getDescriptor(ImageDescriptor.class);
Pair<ImageDescriptor, byte[]> result = processImage(image.getData(), descriptor);
descriptor = result.getLeft();
image.setEtag(descriptor.getEtag());
image.setDescriptorValue(descriptor);
image.setPreview(result.getRight());
log.debug("[{}] Creating image {} ('{}')", image.getTenantId(), image.getResourceKey(), image.getName());
return new TbResourceInfo(doSaveResource(image));
}
private Pair<ImageDescriptor, byte[]> processImage(byte[] data, ImageDescriptor descriptor) throws Exception {
ProcessedImage image = ImageUtils.processImage(data, descriptor.getMediaType(), 250);
ProcessedImage preview = image.getPreview();
descriptor.setWidth(image.getWidth());
descriptor.setHeight(image.getHeight());
descriptor.setSize(image.getSize());
descriptor.setEtag(calculateEtag(data));
ImageDescriptor previewDescriptor = new ImageDescriptor();
previewDescriptor.setWidth(preview.getWidth());
previewDescriptor.setHeight(preview.getHeight());
previewDescriptor.setMediaType(preview.getMediaType());
previewDescriptor.setSize(preview.getSize());
previewDescriptor.setEtag(preview.getData() != null ? calculateEtag(preview.getData()) : descriptor.getEtag());
descriptor.setPreviewDescriptor(previewDescriptor);
return Pair.of(descriptor, preview.getData());
}
private String getUniqueKey(TenantId tenantId, String filename) {
if (!resourceInfoDao.existsByTenantIdAndResourceTypeAndResourceKey(tenantId, ResourceType.IMAGE, filename)) {
return filename;
}
String basename = StringUtils.substringBeforeLast(filename, ".");
String extension = StringUtils.substringAfterLast(filename, ".");
Set<String> existing = resourceInfoDao.findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(
tenantId, ResourceType.IMAGE, basename
);
String resourceKey = filename;
int idx = 1;
while (existing.contains(resourceKey)) {
resourceKey = basename + "_(" + idx + ")." + extension;
idx++;
}
log.debug("[{}] Generated unique key {} for image {}", tenantId, resourceKey, filename);
return resourceKey;
}
@Override
public TbResourceInfo saveImageInfo(TbResourceInfo imageInfo) {
log.trace("Executing saveImageInfo [{}] [{}]", imageInfo.getTenantId(), imageInfo.getId());
return saveResource(new TbResource(imageInfo));
}
@Override
public TbResourceInfo getImageInfoByTenantIdAndKey(TenantId tenantId, String key) {
log.trace("Executing getImageInfoByTenantIdAndKey [{}] [{}]", tenantId, key);
return findResourceInfoByTenantIdAndKey(tenantId, ResourceType.IMAGE, key);
}
@Override
public PageData<TbResourceInfo> getImagesByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing getImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.build();
return findTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> getAllImagesByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing getAllImagesByTenantId [{}]", tenantId);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.resourceTypes(Set.of(ResourceType.IMAGE))
.build();
return findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
public byte[] getImageData(TenantId tenantId, TbResourceId imageId) {
log.trace("Executing getImageData [{}] [{}]", tenantId, imageId);
return resourceDao.getResourceData(tenantId, imageId);
}
@Override
public byte[] getImagePreview(TenantId tenantId, TbResourceId imageId) {
log.trace("Executing getImagePreview [{}] [{}]", tenantId, imageId);
return resourceDao.getResourcePreview(tenantId, imageId);
}
@Override
public TbImageDeleteResult deleteImage(TbResourceInfo imageInfo, boolean force) {
var tenantId = imageInfo.getTenantId();
var imageId = imageInfo.getId();
log.trace("Executing deleteImage [{}] [{}]", tenantId, imageId);
Validator.validateId(imageId, INCORRECT_RESOURCE_ID + imageId);
TbImageDeleteResult.TbImageDeleteResultBuilder result = TbImageDeleteResult.builder();
boolean success = true;
if (!force) {
var link = DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink();
Map<String, List<? extends HasId<?>>> affectedEntities = new HashMap<>();
imageContainerDaoMap.forEach((entityType, imageContainerDao) -> {
var entities = tenantId.isSysTenantId() ? imageContainerDao.findByImageLink(link, MAX_ENTITIES_TO_FIND) :
imageContainerDao.findByTenantAndImageLink(tenantId, link, MAX_ENTITIES_TO_FIND);
if (!entities.isEmpty()) {
affectedEntities.put(entityType.name(), entities);
}
});
if (!affectedEntities.isEmpty()) {
success = false;
result.references(affectedEntities);
}
}
if (success) {
deleteResource(tenantId, imageId, force);
}
return result.success(success).build();
}
@Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, String etag) {
log.trace("Executing findSystemOrTenantImageByEtag [{}] [{}]", tenantId, etag);
return resourceInfoDao.findSystemOrTenantImageByEtag(tenantId, ResourceType.IMAGE, etag);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)// we don't want transaction to rollback in case of an image processing failure
@Override
public boolean replaceBase64WithImageUrl(HasImage entity, String type) {
log.trace("Executing replaceBase64WithImageUrl [{}] [{}] [{}]", entity.getTenantId(), type, entity.getName());
String imageName = "\"" + entity.getName() + "\" ";
if (entity.getTenantId() == null || entity.getTenantId().isSysTenantId()) {
imageName += "system ";
}
imageName = imageName + type + " image";
UpdateResult result = base64ToImageUrl(entity.getTenantId(), imageName, entity.getImage());
entity.setImage(result.getValue());
return result.isUpdated();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)// 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()) {
prefix += "system ";
}
prefix += "widget";
UpdateResult result = base64ToImageUrl(entity.getTenantId(), prefix + " image", entity.getImage());
entity.setImage(result.getValue());
boolean updated = result.isUpdated();
if (entity.getDescriptor().isObject()) {
ObjectNode descriptor = (ObjectNode) entity.getDescriptor();
JsonNode defaultConfig = Optional.ofNullable(descriptor.get("defaultConfig"))
.filter(JsonNode::isTextual).map(JsonNode::asText)
.map(JacksonUtil::toJsonNode).orElse(null);
if (defaultConfig != null) {
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;
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)// 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());
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());
return updated;
}
private boolean base64ToImageUrlUsingMapping(TenantId tenantId, Map<String, String> mapping, Map<String, String> templateParams, JsonNode configuration) {
boolean updated = false;
for (var entry : mapping.entrySet()) {
String expression = entry.getValue();
Queue<JsonPathProcessingTask> tasks = new LinkedList<>();
tasks.add(new JsonPathProcessingTask(entry.getKey().split("\\."), templateParams, configuration));
while (!tasks.isEmpty()) {
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()) {
ArrayNode childArray = (ArrayNode) node;
for (JsonNode element : childArray) {
tasks.add(task.next(element));
}
} else if (node.isObject()) {
ObjectNode on = (ObjectNode) node;
for (Iterator<Map.Entry<String, JsonNode>> it = on.fields(); it.hasNext(); ) {
var kv = it.next();
if (variableName != null) {
tasks.add(task.next(kv.getValue(), variableName, kv.getKey()));
} else {
tasks.add(task.next(kv.getValue()));
}
}
}
} else {
String variableName = null;
String variableValue = null;
if (token.contains("[$")) {
variableName = StringUtils.substringBetween(token, "[$", "]");
token = StringUtils.substringBefore(token, "[$");
}
if (node.has(token)) {
JsonNode value = node.get(token);
if (variableName != null && value.has(variableName) && value.get(variableName).isTextual()) {
variableValue = value.get(variableName).asText();
}
if (task.isLast()) {
String name = expression;
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());
((ObjectNode) node).put(token, result.getValue());
updated |= result.isUpdated();
} else if (value.isArray()) {
ArrayNode array = (ArrayNode) value;
for (int i = 0; i < array.size(); i++) {
String arrayElementName = name.replace("$index", Integer.toString(i));
UpdateResult result = base64ToImageUrl(tenantId, arrayElementName, array.get(i).asText());
array.set(i, result.getValue());
updated |= result.isUpdated();
}
}
} else {
if (StringUtils.isNotEmpty(variableName)) {
tasks.add(task.next(value, variableName, variableValue));
} else {
tasks.add(task.next(value));
}
}
}
}
}
}
return updated;
}
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data) {
return base64ToImageUrl(tenantId, name, data, false);
}
private static final Pattern TB_IMAGE_METADATA_PATTERN = Pattern.compile("^tb-image:(.*):(.*);data:(.*);.*");
private UpdateResult base64ToImageUrl(TenantId tenantId, String name, String data, boolean strict) {
if (StringUtils.isBlank(data)) {
return UpdateResult.of(false, data);
}
var matcher = TB_IMAGE_METADATA_PATTERN.matcher(data);
boolean matches = matcher.matches();
String mdResourceKey = null;
String mdResourceName = null;
String mdMediaType;
if (matches) {
mdResourceKey = new String(Base64Utils.decodeFromString(matcher.group(1)), StandardCharsets.UTF_8);
mdResourceName = new String(Base64Utils.decodeFromString(matcher.group(2)), StandardCharsets.UTF_8);
mdMediaType = matcher.group(3);
} else if (data.startsWith(DataConstants.TB_IMAGE_PREFIX + "data:image/") || (!strict && data.startsWith("data:image/"))) {
mdMediaType = StringUtils.substringBetween(data, "data:", ";base64");
} else {
return UpdateResult.of(false, data);
}
String base64Data = StringUtils.substringAfter(data, "base64,");
String extension = ImageUtils.mediaTypeToFileExtension(mdMediaType);
byte[] imageData = Base64.getDecoder().decode(base64Data);
String etag = calculateEtag(imageData);
var imageInfo = findSystemOrTenantImageByEtag(tenantId, etag);
if (imageInfo == null) {
TbResource image = new TbResource();
image.setTenantId(tenantId);
image.setResourceType(ResourceType.IMAGE);
if (StringUtils.isBlank(mdResourceName)) {
mdResourceName = name;
}
image.setTitle(mdResourceName);
String fileName;
if (StringUtils.isBlank(mdResourceKey)) {
fileName = StringUtils.strip(mdResourceName.toLowerCase()
.replaceAll("['\"]", "")
.replaceAll("[^\\pL\\d]+", "_"), "_") // leaving only letters and numbers
+ "." + extension;
} else {
fileName = mdResourceKey;
}
image.setFileName(fileName);
image.setDescriptor(JacksonUtil.newObjectNode().put("mediaType", mdMediaType));
image.setData(imageData);
try {
imageInfo = saveImage(image);
} catch (Exception e) {
if (log.isDebugEnabled()) { // printing stacktrace
log.warn("[{}][{}] Failed to replace Base64 with image url for {}", tenantId, name, StringUtils.abbreviate(data, 50), e);
} else {
log.warn("[{}][{}] Failed to replace Base64 with image url for {}: {}", tenantId, name, StringUtils.abbreviate(data, 50), ExceptionUtils.getMessage(e));
}
return UpdateResult.of(false, data);
}
}
return UpdateResult.of(true, DataConstants.TB_IMAGE_PREFIX + imageInfo.getLink());
}
private boolean base64ToImageUrlRecursively(TenantId tenantId, String title, JsonNode root) {
boolean updated = false;
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>();
tasks.add(new JsonNodeProcessingTask(title, root));
while (!tasks.isEmpty()) {
JsonNodeProcessingTask task = tasks.poll();
JsonNode node = task.getNode();
if (node == null) {
continue;
}
String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + " ");
if (node.isObject()) {
ObjectNode on = (ObjectNode) node;
for (Iterator<String> it = on.fieldNames(); it.hasNext(); ) {
String childName = it.next();
JsonNode childValue = on.get(childName);
if (childValue.isTextual()) {
UpdateResult result = base64ToImageUrl(tenantId, currentPath + childName, childValue.asText(), true);
on.put(childName, result.getValue());
updated |= result.isUpdated();
} else if (childValue.isObject() || childValue.isArray()) {
tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue));
}
}
} else if (node.isArray()) {
ArrayNode childArray = (ArrayNode) node;
for (int i = 0; i < childArray.size(); i++) {
JsonNode element = childArray.get(i);
if (element.isObject()) {
tasks.add(new JsonNodeProcessingTask(currentPath + " " + i, element));
} else if (element.isTextual()) {
UpdateResult result = base64ToImageUrl(tenantId, currentPath + "." + i, element.asText(), true);
childArray.set(i, result.getValue());
updated |= result.isUpdated();
}
}
}
}
return updated;
}
@Override
public void inlineImage(HasImage entity) {
log.trace("Executing inlineImage [{}] [{}] [{}]", entity.getTenantId(), entity.getClass().getSimpleName(), entity.getName());
entity.setImage(inlineImage(entity.getTenantId(), "image", entity.getImage()));
}
@Override
public void inlineImages(Dashboard dashboard) {
log.trace("Executing inlineImage [{}] [Dashboard] [{}]", dashboard.getTenantId(), dashboard.getId());
inlineImage(dashboard);
inlineIntoJson(dashboard.getTenantId(), dashboard.getConfiguration());
}
@Override
public void inlineImages(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing inlineImage [{}] [WidgetTypeDetails] [{}]", widgetTypeDetails.getTenantId(), widgetTypeDetails.getId());
inlineImage(widgetTypeDetails);
inlineIntoJson(widgetTypeDetails.getTenantId(), widgetTypeDetails.getDescriptor());
}
private void inlineIntoJson(TenantId tenantId, JsonNode root) {
Queue<JsonNodeProcessingTask> tasks = new LinkedList<>();
tasks.add(new JsonNodeProcessingTask("", root));
while (!tasks.isEmpty()) {
JsonNodeProcessingTask task = tasks.poll();
JsonNode node = task.getNode();
if (node == null) {
continue;
}
String currentPath = StringUtils.isBlank(task.getPath()) ? "" : (task.getPath() + ".");
if (node.isObject()) {
ObjectNode on = (ObjectNode) node;
for (Iterator<String> it = on.fieldNames(); it.hasNext(); ) {
String childName = it.next();
JsonNode childValue = on.get(childName);
if (childValue.isTextual()) {
on.put(childName, inlineImage(tenantId, currentPath + childName, childValue.asText()));
} else if (childValue.isObject() || childValue.isArray()) {
tasks.add(new JsonNodeProcessingTask(currentPath + childName, childValue));
}
}
} else if (node.isArray()) {
ArrayNode childArray = (ArrayNode) node;
for (int i = 0; i < childArray.size(); i++) {
JsonNode element = childArray.get(i);
if (element.isObject()) {
tasks.add(new JsonNodeProcessingTask(currentPath + "." + i, element));
} else if (element.isTextual()) {
childArray.set(i, inlineImage(tenantId, currentPath + "." + i, element.asText()));
}
}
}
}
}
private String inlineImage(TenantId tenantId, String path, String url) {
try {
ImageCacheKey key = getKeyFromUrl(tenantId, url);
if (key != null) {
var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getKey());
if (imageInfo != null) {
byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId());
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
String tbImagePrefix = "tb-image:" + Base64Utils.encodeToString(imageInfo.getResourceKey().getBytes(StandardCharsets.UTF_8)) + ":"
+ Base64Utils.encodeToString(imageInfo.getName().getBytes(StandardCharsets.UTF_8)) + ";";
return tbImagePrefix + "data:" + descriptor.getMediaType() + ";base64," + Base64Utils.encodeToString(data);
}
}
} catch (Exception e) {
log.warn("[{}][{}][{}] Failed to inline image.", tenantId, path, url, e);
}
return url;
}
private ImageDescriptor getImageDescriptor(TbResourceInfo imageInfo, boolean preview) throws JsonProcessingException {
ImageDescriptor descriptor = imageInfo.getDescriptor(ImageDescriptor.class);
return preview ? descriptor.getPreviewDescriptor() : descriptor;
}
private ImageCacheKey getKeyFromUrl(TenantId tenantId, String url) {
if (StringUtils.isBlank(url)) {
return null;
}
TenantId imageTenantId = null;
if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/tenant/")) {
imageTenantId = tenantId;
} else if (url.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images/system/")) {
imageTenantId = TenantId.SYS_TENANT_ID;
}
if (imageTenantId != null) {
var parts = url.split("/");
if (parts.length == 5) {
return new ImageCacheKey(imageTenantId, parts[4], false);
} else if (parts.length == 6 && "preview".equals(parts[5])) {
return new ImageCacheKey(imageTenantId, parts[4], true);
}
}
return null;
}
@Data(staticConstructor = "of")
private static class UpdateResult {
private final boolean updated;
private final String value;
}
}

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

@ -15,10 +15,12 @@
*/
package org.thingsboard.server.dao.resource;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
@ -38,9 +40,9 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import java.util.List;
import java.util.Optional;
@ -51,49 +53,64 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService")
@Slf4j
@AllArgsConstructor
@Primary
public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
private final TbResourceDao resourceDao;
private final TbResourceInfoDao resourceInfoDao;
private final DataValidator<TbResource> resourceValidator;
protected final TbResourceDao resourceDao;
protected final TbResourceInfoDao resourceInfoDao;
protected final ResourceDataValidator resourceValidator;
@Override
public TbResource saveResource(TbResource resource) {
return doSaveResource(resource, true);
public TbResource saveResource(TbResource resource, boolean doValidate) {
log.trace("Executing saveResource [{}]", resource);
if (doValidate) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId);
}
if (resource.getData() != null) {
resource.setEtag(calculateEtag(resource.getData()));
}
return doSaveResource(resource);
}
@Override
public TbResource saveResource(TbResource resource, boolean doValidate) {
return doSaveResource(resource, doValidate);
public TbResource saveResource(TbResource resource) {
return saveResource(resource, true);
}
private TbResource doSaveResource(TbResource resource, boolean doValidate) {
if (doValidate) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId);
}
protected TbResource doSaveResource(TbResource resource) {
TenantId tenantId = resource.getTenantId();
try {
TbResource saved = resourceDao.save(resource.getTenantId(), resource);
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
TbResource saved;
if (resource.getData() != null) {
saved = resourceDao.save(tenantId, resource);
} else {
TbResourceInfo resourceInfo = saveResourceInfo(resource);
saved = new TbResource(resourceInfo);
}
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId()));
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(saved.getTenantId())
.entityId(saved.getId()).added(resource.getId() == null).build());
return saved;
} catch (Exception t) {
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resource.getId()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) {
String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName";
throw new DataValidationException("Resource with such " + field + " already exists!");
throw new DataValidationException("Resource with such key already exists!");
} else {
throw t;
}
}
}
private TbResourceInfo saveResourceInfo(TbResource resource) {
return resourceInfoDao.save(resource.getTenantId(), new TbResourceInfo(resource));
}
@Override
public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey) {
log.trace("Executing getResource [{}] [{}] [{}]", tenantId, resourceType, resourceKey);
return resourceDao.getResource(tenantId, resourceType, resourceKey);
public TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
log.trace("Executing findResourceByTenantIdAndKey [{}] [{}] [{}]", tenantId, resourceType, resourceKey);
return resourceDao.findResourceByTenantIdAndKey(tenantId, resourceType, resourceKey);
}
@Override
@ -112,6 +129,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
() -> resourceInfoDao.findById(tenantId, resourceId.getId()), true);
}
@Override
public TbResourceInfo findResourceInfoByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
log.trace("Executing findResourceInfoByTenantIdAndKey [{}] [{}] [{}]", tenantId, resourceType, resourceKey);
return resourceInfoDao.findByTenantIdAndKey(tenantId, resourceType, resourceKey);
}
@Override
public ListenableFuture<TbResourceInfo> findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
@ -121,13 +144,21 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public void deleteResource(TenantId tenantId, TbResourceId resourceId) {
deleteResource(tenantId, resourceId, false);
}
@Override
public void deleteResource(TenantId tenantId, TbResourceId resourceId, boolean force) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
resourceValidator.validateDelete(tenantId, resourceId);
if (!force) {
resourceValidator.validateDelete(tenantId, resourceId);
}
resourceDao.removeById(tenantId, resourceId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(resourceId).build());
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
TenantId tenantId = filter.getTenantId();
@ -177,6 +208,11 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return Optional.ofNullable(findResourceInfoById(tenantId, new TbResourceId(entityId.getId())));
}
@Override
public void deleteEntity(TenantId tenantId, EntityId id) {
deleteResource(tenantId, (TbResourceId) id);
}
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;
@ -187,6 +223,10 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.sumDataSizeByTenantId(tenantId);
}
protected String calculateEtag(byte[] data) {
return Hashing.sha256().hashBytes(data).toString();
}
private final PaginatedRemover<TenantId, TbResource> tenantResourcesRemover =
new PaginatedRemover<>() {

28
dao/src/main/java/org/thingsboard/server/dao/resource/ImageCacheKey.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.resource;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class ImageCacheKey {
private final TenantId tenantId;
private final String key;
private final boolean preview;
}

13
dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java

@ -17,17 +17,19 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
import java.util.List;
public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao {
public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao, ExportableEntityDao<TbResourceId, TbResource> {
TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId);
TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceId);
PageData<TbResource> findAllByTenantId(TenantId tenantId, PageLink pageLink);
@ -39,4 +41,11 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao
ResourceType resourceType,
String[] objectIds,
String searchText);
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId);
long getResourceSize(TenantId tenantId, TbResourceId resourceId);
}

14
dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java

@ -15,13 +15,16 @@
*/
package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import java.util.UUID;
import java.util.List;
import java.util.Set;
public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
@ -29,4 +32,13 @@ public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey);
Set<String> findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(TenantId tenantId, ResourceType resourceType, String prefix);
List<TbResourceInfo> findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query);
TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag);
}

5
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -129,8 +129,7 @@ public abstract class DataValidator<D extends BaseData<?>> {
EntityType entityType) {
if (maxSumDataSize > 0) {
if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) {
throw new DataValidationException(String.format("Failed to create the %s, files size limit is exhausted %d bytes!",
entityType.name().toLowerCase().replaceAll("_", " "), maxSumDataSize));
throw new DataValidationException(String.format("%ss total size exceeds the maximum of " + maxSumDataSize + " bytes", entityType.getNormalName()));
}
}
}
@ -162,7 +161,7 @@ public abstract class DataValidator<D extends BaseData<?>> {
}
static void validateQueueNameOrTopic(String value, String fieldName) {
if (StringUtils.isEmpty(value) || value.trim().length() == 0 ) {
if (StringUtils.isEmpty(value) || value.trim().length() == 0) {
throw new DataValidationException(String.format("Queue %s should be specified!", fieldName));
}
if (!QUEUE_PATTERN.matcher(value).matches()) {

52
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -15,10 +15,10 @@
*/
package org.thingsboard.server.dao.service.validator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@ -56,34 +55,57 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Override
protected void validateCreate(TenantId tenantId, TbResource resource) {
if (tenantId != null && !TenantId.SYS_TENANT_ID.equals(tenantId)) {
DefaultTenantProfileConfiguration profileConfiguration =
(DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration();
long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, resource.getData().length(), TB_RESOURCE);
if (resource.getData() == null || resource.getData().length == 0) {
throw new DataValidationException("Resource data should be specified");
}
}
@Override
protected TbResource validateUpdate(TenantId tenantId, TbResource resource) {
if (resource.getData() != null && !resource.getResourceType().isUpdatable() &&
tenantId != null && !tenantId.isSysTenantId()) {
throw new DataValidationException("This type of resource can't be updated");
}
return resource;
}
@Override
protected void validateDataImpl(TenantId tenantId, TbResource resource) {
validateString("Resource title", resource.getTitle());
if (resource.getTenantId() == null) {
resource.setTenantId(TenantId.SYS_TENANT_ID);
}
if (!resource.getTenantId().isSysTenantId()) {
if (!tenantService.tenantExists(resource.getTenantId())) {
throw new DataValidationException("Resource is referencing to non-existent tenant!");
}
}
if (resource.getResourceType() == null) {
throw new DataValidationException("Resource type should be specified!");
}
if (!resource.getTenantId().isSysTenantId() && resource.getData() != null) {
DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
long maxResourceSize = profileConfiguration.getMaxResourceSize();
if (maxResourceSize > 0 && resource.getData().length > maxResourceSize) {
throw new IllegalArgumentException("Resource exceeds the maximum size of " + maxResourceSize + " bytes");
}
long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
int dataSize = resource.getData().length;
if (resource.getId() != null) {
long prevSize = resourceDao.getResourceSize(tenantId, resource.getId());
dataSize -= prevSize;
}
validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, dataSize, TB_RESOURCE);
}
if (StringUtils.isEmpty(resource.getFileName())) {
throw new DataValidationException("Resource file name should be specified!");
}
if (StringUtils.containsAny(resource.getFileName(), "/", "\\")) {
throw new DataValidationException("File name contains forbidden symbols");
}
if (StringUtils.isEmpty(resource.getResourceKey())) {
throw new DataValidationException("Resource key should be specified!");
}
if (resource.getTenantId() == null) {
resource.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID));
}
if (!resource.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
if (!tenantService.tenantExists(resource.getTenantId())) {
throw new DataValidationException("Resource is referencing to non-existent tenant!");
}
}
}
@Override

11
dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.AssetProfileEntity;
import java.util.List;
import java.util.UUID;
public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity, UUID>, ExportableEntityRepository<AssetProfileEntity> {
@ -60,4 +61,14 @@ public interface AssetProfileRepository extends JpaRepository<AssetProfileEntity
@Query("SELECT externalId FROM AssetProfileEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " +
"FROM AssetProfileEntity a WHERE a.tenantId = :tenantId AND a.image = :imageLink")
List<AssetProfileInfo> findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, Pageable page);
@Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.tenantId, a.name, a.image, a.defaultDashboardId) " +
"FROM AssetProfileEntity a WHERE a.image = :imageLink")
List<AssetProfileInfo> findByImageLink(@Param("imageLink") String imageLink, Pageable page);
Page<AssetProfileEntity> findAllByImageNotNull(Pageable pageable);
}

18
dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.asset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@ -31,7 +32,7 @@ import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.model.sql.AssetProfileEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import java.util.Objects;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -97,6 +98,11 @@ public class JpaAssetProfileDao extends JpaAbstractDao<AssetProfileEntity, Asset
return DaoUtil.getData(assetProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName));
}
@Override
public PageData<AssetProfile> findAllWithImages(PageLink pageLink) {
return DaoUtil.toPageData(assetProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink)));
}
@Override
public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId));
@ -118,6 +124,16 @@ public class JpaAssetProfileDao extends JpaAbstractDao<AssetProfileEntity, Asset
.map(AssetProfileId::new).orElse(null);
}
@Override
public List<AssetProfileInfo> findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) {
return assetProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit));
}
@Override
public List<AssetProfileInfo> findByImageLink(String imageLink, int limit) {
return assetProfileRepository.findByImageLink(imageLink, PageRequest.of(0, limit));
}
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;

13
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java

@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.DashboardInfoEntity;
import java.util.List;
import java.util.UUID;
/**
@ -74,4 +75,16 @@ public interface DashboardInfoRepository extends JpaRepository<DashboardInfoEnti
@Query("SELECT di.title FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId AND di.id = :dashboardId")
String findTitleByTenantIdAndId(@Param("tenantId") UUID tenantId, @Param("dashboardId") UUID dashboardId);
@Query(nativeQuery = true,
value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId " +
"and (d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%')) limit :lmt"
)
List<DashboardInfoEntity> findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, @Param("lmt") int lmt);
@Query(nativeQuery = true,
value = "SELECT * FROM dashboard d WHERE d.image = :imageLink or d.configuration ILIKE CONCAT('%\"', :imageLink, '\"%') limit :lmt"
)
List<DashboardInfoEntity> findByImageLink(@Param("imageLink") String imageLink, @Param("lmt") int lmt);
}

3
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java

@ -43,4 +43,7 @@ public interface DashboardRepository extends JpaRepository<DashboardEntity, UUID
@Query("SELECT d.id FROM DashboardEntity d WHERE d.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
@Query("SELECT d.id FROM DashboardEntity d")
Page<UUID> findAllIds(Pageable pageable);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java

@ -85,6 +85,11 @@ public class JpaDashboardDao extends JpaAbstractDao<DashboardEntity, Dashboard>
return DaoUtil.pageToPageData(dashboardRepository.findIdsByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)).map(DashboardId::new));
}
@Override
public PageData<DashboardId> findAllIds(PageLink pageLink) {
return DaoUtil.pageToPageData(dashboardRepository.findAllIds(DaoUtil.toPageable(pageLink)).map(DashboardId::new));
}
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;

12
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java

@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@ -31,7 +32,6 @@ import org.thingsboard.server.dao.util.SqlDao;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
@ -123,4 +123,14 @@ public class JpaDashboardInfoDao extends JpaAbstractDao<DashboardInfoEntity, Das
public String findTitleById(UUID tenantId, UUID dashboardId) {
return dashboardInfoRepository.findTitleByTenantIdAndId(tenantId, dashboardId);
}
@Override
public List<DashboardInfo> findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) {
return DaoUtil.convertDataList(dashboardInfoRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, limit));
}
@Override
public List<DashboardInfo> findByImageLink(String imageLink, int limit) {
return DaoUtil.convertDataList(dashboardInfoRepository.findByImageLink(imageLink, limit));
}
}

12
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -22,9 +22,11 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import java.util.List;
import java.util.UUID;
public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEntity, UUID>, ExportableEntityRepository<DeviceProfileEntity> {
@ -68,7 +70,17 @@ public interface DeviceProfileRepository extends JpaRepository<DeviceProfileEnti
DeviceProfileEntity findByProvisionDeviceKey(@Param("provisionDeviceKey") String provisionDeviceKey);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d WHERE d.tenantId = :tenantId AND d.image = :imageLink")
List<DeviceProfileInfo> findByTenantAndImageLink(@Param("tenantId") UUID tenantId, @Param("imageLink") String imageLink, Pageable page);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.tenantId, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " +
"FROM DeviceProfileEntity d WHERE d.image = :imageLink")
List<DeviceProfileInfo> findByImageLink(@Param("imageLink") String imageLink, Pageable page);
@Query("SELECT externalId FROM DeviceProfileEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
Page<DeviceProfileEntity> findAllByImageNotNull(Pageable pageable);
}

18
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.device;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@ -34,7 +35,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.Objects;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@ -115,6 +116,11 @@ public class JpaDeviceProfileDao extends JpaAbstractDao<DeviceProfileEntity, Dev
return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName));
}
@Override
public PageData<DeviceProfile> findAllWithImages(PageLink pageLink) {
return DaoUtil.toPageData(deviceProfileRepository.findAllByImageNotNull(DaoUtil.toPageable(pageLink)));
}
@Override
public DeviceProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(deviceProfileRepository.findByTenantIdAndExternalId(tenantId, externalId));
@ -136,6 +142,16 @@ public class JpaDeviceProfileDao extends JpaAbstractDao<DeviceProfileEntity, Dev
.map(DeviceProfileId::new).orElse(null);
}
@Override
public List<DeviceProfileInfo> findByTenantAndImageLink(TenantId tenantId, String imageLink, int limit) {
return deviceProfileRepository.findByTenantAndImageLink(tenantId.getId(), imageLink, PageRequest.of(0, limit));
}
@Override
public List<DeviceProfileInfo> findByImageLink(String imageLink, int limit) {
return deviceProfileRepository.findByImageLink(imageLink, PageRequest.of(0, limit));
}
@Override
public EntityType getEntityType() {
return EntityType.DEVICE_PROFILE;

41
dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java

@ -21,6 +21,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -31,7 +32,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@ -56,8 +56,7 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
}
@Override
public TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey) {
public TbResource findResourceByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
return DaoUtil.getData(resourceRepository.findByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey));
}
@ -95,11 +94,47 @@ public class JpaTbResourceDao extends JpaAbstractDao<TbResourceEntity, TbResourc
resourceType.name(), objectIds));
}
@Override
public byte[] getResourceData(TenantId tenantId, TbResourceId resourceId) {
return resourceRepository.getDataById(resourceId.getId());
}
@Override
public byte[] getResourcePreview(TenantId tenantId, TbResourceId resourceId) {
return resourceRepository.getPreviewById(resourceId.getId());
}
@Override
public long getResourceSize(TenantId tenantId, TbResourceId resourceId) {
return resourceRepository.getDataSizeById(resourceId.getId());
}
@Override
public Long sumDataSizeByTenantId(TenantId tenantId) {
return resourceRepository.sumDataSizeByTenantId(tenantId.getId());
}
@Override
public TbResource findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(resourceRepository.findByTenantIdAndExternalId(tenantId, externalId));
}
@Override
public PageData<TbResource> findByTenantId(UUID tenantId, PageLink pageLink) {
return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink);
}
@Override
public PageData<TbResourceId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(resourceRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink))
.map(TbResourceId::new));
}
@Override
public TbResourceId getExternalIdByInternal(TbResourceId internalId) {
return DaoUtil.toEntityId(resourceRepository.getExternalIdByInternal(internalId.getId()), TbResourceId::new);
}
@Override
public EntityType getEntityType() {
return EntityType.TB_RESOURCE;

46
dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java

@ -25,14 +25,19 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity;
import org.thingsboard.server.dao.resource.TbResourceInfoDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -54,25 +59,54 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
Set<ResourceType> resourceTypes = filter.getResourceTypes();
if (CollectionsUtil.isEmpty(resourceTypes)) {
resourceTypes = EnumSet.allOf(ResourceType.class);
}
return DaoUtil.toPageData(resourceInfoRepository
.findAllTenantResourcesByTenantId(
filter.getTenantId().getId(),
TenantId.NULL_UUID,
resourceType == null ? null : resourceType.name(),
filter.getTenantId().getId(), TenantId.NULL_UUID,
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
Set<ResourceType> resourceTypes = filter.getResourceTypes();
if (CollectionsUtil.isEmpty(resourceTypes)) {
resourceTypes = EnumSet.allOf(ResourceType.class);
}
return DaoUtil.toPageData(resourceInfoRepository
.findTenantResourcesByTenantId(
filter.getTenantId().getId(),
resourceType == null ? null : resourceType.name(),
resourceTypes.stream().map(Enum::name).collect(Collectors.toList()),
pageLink.getTextSearch(),
DaoUtil.toPageable(pageLink)));
}
@Override
public TbResourceInfo findByTenantIdAndKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
return DaoUtil.getData(resourceInfoRepository.findByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey));
}
@Override
public boolean existsByTenantIdAndResourceTypeAndResourceKey(TenantId tenantId, ResourceType resourceType, String resourceKey) {
return resourceInfoRepository.existsByTenantIdAndResourceTypeAndResourceKey(tenantId.getId(), resourceType.name(), resourceKey);
}
@Override
public Set<String> findKeysByTenantIdAndResourceTypeAndResourceKeyPrefix(TenantId tenantId, ResourceType resourceType, String prefix) {
return resourceInfoRepository.findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(tenantId.getId(), resourceType.name(), prefix);
}
@Override
public List<TbResourceInfo> findByTenantIdAndEtagAndKeyStartingWith(TenantId tenantId, String etag, String query) {
return DaoUtil.convertDataList(resourceInfoRepository.findByTenantIdAndEtagAndResourceKeyStartingWith(tenantId.getId(), etag, query));
}
@Override
public TbResourceInfo findSystemOrTenantImageByEtag(TenantId tenantId, ResourceType resourceType, String etag) {
return DaoUtil.getData(resourceInfoRepository.findSystemOrTenantImageByEtag(tenantId.getId(), resourceType.name(), etag));
}
}

31
dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java

@ -22,6 +22,8 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.TbResourceInfoEntity;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEntity, UUID> {
@ -29,26 +31,43 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
@Query("SELECT tr FROM TbResourceInfoEntity tr WHERE " +
"(:searchText IS NULL OR ilike(tr.title, CONCAT('%', :searchText, '%')) = true) " +
"AND (tr.tenantId = :tenantId " +
"OR (tr.tenantId = :systemAdminId " +
"OR (tr.tenantId = :systemTenantId " +
"AND NOT EXISTS " +
"(SELECT sr FROM TbResourceEntity sr " +
"WHERE sr.tenantId = :tenantId " +
"AND tr.resourceType = sr.resourceType " +
"AND tr.resourceKey = sr.resourceKey)))" +
"AND (:resourceType IS NULL OR tr.resourceType = :resourceType)")
"AND tr.resourceType IN :resourceTypes")
Page<TbResourceInfoEntity> findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysadminId,
@Param("resourceType") String resourceType,
@Param("systemTenantId") UUID systemTenantId,
@Param("resourceTypes") List<String> resourceTypes,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
"ri.tenantId = :tenantId " +
"AND (:resourceType IS NULL OR ri.resourceType = :resourceType)" +
"AND ri.resourceType IN :resourceTypes " +
"AND (:searchText IS NULL OR ilike(ri.title, CONCAT('%', :searchText, '%')) = true)")
Page<TbResourceInfoEntity> findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("resourceTypes") List<String> resourceTypes,
@Param("searchText") String searchText,
Pageable pageable);
TbResourceInfoEntity findByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey);
boolean existsByTenantIdAndResourceTypeAndResourceKey(UUID tenantId, String resourceType, String resourceKey);
@Query(value = "SELECT r.resource_key FROM resource r WHERE r.tenant_id = :tenantId AND r.resource_type = :resourceType " +
"AND starts_with(r.resource_key, :prefix)", nativeQuery = true)
Set<String> findKeysByTenantIdAndResourceTypeAndResourceKeyStartingWith(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("prefix") String prefix);
List<TbResourceInfoEntity> findByTenantIdAndEtagAndResourceKeyStartingWith(UUID tenantId, String etag, String query);
@Query(value = "SELECT * FROM resource r WHERE (r.tenant_id = '13814000-1dd2-11b2-8080-808080808080' OR r.tenant_id = :tenantId) " +
"AND r.resource_type = :resourceType AND r.etag = :etag LIMIT 1", nativeQuery = true)
TbResourceInfoEntity findSystemOrTenantImageByEtag(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("etag") String etag);
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save