From 27473619dbc11e9a9f5598a5cea61f7f34b59496 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 09:54:33 +0300 Subject: [PATCH 01/14] Ota package vc support --- .../main/data/upgrade/basic/schema_update.sql | 49 ++--------- .../DefaultEntitiesExportImportService.java | 2 +- .../exporting/impl/DeviceExportService.java | 2 + .../impl/DeviceProfileExportService.java | 2 + .../impl/OtaPackageExportService.java | 44 ++++++++++ .../impl/BaseEntityImportService.java | 15 ++-- .../importing/impl/DeviceImportService.java | 4 +- .../impl/DeviceProfileImportService.java | 6 +- .../impl/OtaPackageImportService.java | 83 +++++++++++++++++++ .../sync/ie/ExportImportServiceSqlTest.java | 29 +++++-- .../service/sync/vc/VersionControlTest.java | 75 +++++++++++++++-- .../server/dao/ota/OtaPackageService.java | 3 + .../server/common/data/OtaPackage.java | 3 + .../server/common/data/OtaPackageInfo.java | 22 ++++- .../server/common/data/TbResource.java | 3 + .../server/common/data/sync/JsonTbEntity.java | 4 +- .../dao/model/sql/OtaPackageEntity.java | 7 ++ .../dao/model/sql/OtaPackageInfoEntity.java | 9 +- .../server/dao/ota/BaseOtaPackageService.java | 37 ++++----- .../server/dao/ota/OtaPackageDao.java | 7 +- .../server/dao/sql/ota/JpaOtaPackageDao.java | 46 ++++++++-- .../dao/sql/ota/OtaPackageInfoRepository.java | 7 +- .../dao/sql/ota/OtaPackageRepository.java | 12 ++- .../resources/sql/schema-entities-idx.sql | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 25 files changed, 370 insertions(+), 107 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e4bbe4c69e..2f652ce289 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,48 +14,11 @@ -- limitations under the License. -- --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START +-- UPDATE OTA PACKAGE EXTERNAL ID START -UPDATE tenant_profile -SET profile_data = jsonb_set( - profile_data, - '{configuration}', - ( - (profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration' - || - COALESCE( - CASE - WHEN profile_data -> 'configuration' -> - 'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN - jsonb_build_object( - 'cassandraReadQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraReadQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration' - ) - END, - '{}'::jsonb - ) - ) - ) -WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration'; +ALTER TABLE ota_package + ADD COLUMN IF NOT EXISTS external_id uuid; +ALTER TABLE ota_package + ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START - -UPDATE notification_rule -SET trigger_config = REGEXP_REPLACE( - trigger_config, - '"CASSANDRA_QUERIES"', - '"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"', - 'g' - ) -WHERE trigger_type = 'RATE_LIMITS' - AND trigger_config LIKE '%"CASSANDRA_QUERIES"%'; - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END +-- UPDATE OTA PACKAGE EXTERNAL ID END diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index db7e37b368..ee9f9e3cea 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -67,7 +67,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, - EntityType.DEVICE_PROFILE, EntityType.DEVICE, + EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 7d5f7ee57e..2f5355f637 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId())); + device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId())); + device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId())); if (ctx.getSettings().isExportCredentials()) { var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId()); credentials.setId(null); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java index 6c212f0684..6a98bd2a7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java @@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.OTA_PACKAGE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 57b4737be6..92fdcb09c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -71,7 +71,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -148,6 +147,7 @@ public abstract class BaseEntityImportService importResult, D exportData, IdProvider idProvider) throws ThingsboardException { E savedEntity = importResult.getSavedEntity(); E oldEntity = importResult.getOldEntity(); @@ -405,7 +404,9 @@ public abstract class BaseEntityImportService ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) { - if (externalId == null || externalId.isNullUid()) return null; + if (externalId == null || externalId.isNullUid()) { + return null; + } if (EntityType.TENANT.equals(externalId.getEntityType())) { return (ID) ctx.getTenantId(); @@ -432,7 +433,9 @@ public abstract class BaseEntityImportService getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set hints) { - if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); + if (externalUuid.equals(EntityId.NULL_UUID)) { + return Optional.empty(); + } for (EntityType entityType : EntityType.values()) { Optional externalId = buildEntityId(entityType, externalUuid); @@ -483,10 +486,6 @@ public abstract class BaseEntityImportService T getOldEntityField(O oldEntity, Function getter) { - return oldEntity == null ? null : getter.apply(oldEntity); - } - protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, Set skippedRootFields, Pattern includedFieldsPattern, LinkedHashSet hints) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 4ace9ff938..0cbb471b6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService> { + + private final OtaPackageService otaPackageService; + + @Override + protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) { + otaPackage.setTenantId(tenantId); + } + + @Override + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, EntityExportData exportData, IdProvider idProvider) { + otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); + return otaPackage; + } + + @Override + protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) { + OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider); + if (existingOtaPackage == null && ctx.isFindExistingByName()) { + existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitle(ctx.getTenantId(), otaPackage.getType(), otaPackage.getTitle()); + } + return existingOtaPackage; + } + + @Override + protected OtaPackage deepCopy(OtaPackage otaPackage) { + return new OtaPackage(otaPackage); + } + + @Override + protected void cleanupForComparison(OtaPackage otaPackage) { + super.cleanupForComparison(otaPackage); + } + + @Override + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { + return otaPackageService.saveOtaPackage(otaPackage); + } + + @Override + protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { + super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); + } + + @Override + public EntityType getEntityType() { + return EntityType.OTA_PACKAGE; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index da3b214afb..e70a0cd37c 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; @@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1"); Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() @@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any()); verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false)); + OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware), + any(), eq(ActionType.ADDED), isNull()); + Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity(); verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice), any(), eq(ActionType.ADDED), isNull()); verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull()); importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId()); // calculated field of imported device: List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); @@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assetProfile = assetProfileService.saveAssetProfile(assetProfile); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); Map ids = new HashMap<>(); for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), - deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { + deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) @@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); + OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity(); + assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId()); + assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId()); + + EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); Device exportedDevice = entityExportData.getEntity(); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); + assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId()); - List calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + List calculatedFields = entityExportData.getCalculatedFields(); assertThat(calculatedFields.size()).isOne(); CalculatedField field = calculatedFields.get(0); assertThat(field.getName()).isEqualTo(calculatedField.getName()); @@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { + protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) { Device device = new Device(); device.setTenantId(tenantId); device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); + device.setFirmwareId(firmwareId); + device.setSoftwareId(softwareId); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); device.setDeviceData(deviceData); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c7d4068210..5c5b7bb107 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -116,8 +116,8 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA; import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME; +import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA; @DaoSqlTest public class VersionControlTest extends AbstractControllerTest { @@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testDeviceVc_withProfile_betweenTenants() throws Exception { + public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); createVersion("profiles", EntityType.DEVICE_PROFILE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1"); - String versionId = createVersion("devices", EntityType.DEVICE); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { + newDevice.setFirmwareId(firmware.getId()); + newDevice.setSoftwareId(software.getId()); + }); + String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE); DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId()); DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials); newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import doPost("/api/device/credentials", newCredentials, DeviceCredentials.class); - assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles"); + assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles"); loginTenant2(); - Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE); + Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); @@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId()); assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue()); assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType()); + + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); } @Test @@ -653,6 +665,52 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType()); } + @Test + public void testOtaPackageVc_sameTenant() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loadVersion(versionId, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + @Test + public void testOtaPackageVc_betweenTenants() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loginTenant2(); + loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) { + assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName()); + assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag()); + assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType()); + assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName()); + } + @Test public void testResourceVc_sameTenant() throws Exception { TbResourceInfo resourceInfo = createResource("Test resource"); @@ -923,6 +981,7 @@ public class VersionControlTest extends AbstractControllerTest { otaPackage.setDeviceProfileId(deviceProfileId); otaPackage.setType(type); otaPackage.setTitle("My " + type); + otaPackage.setTag("My " + type); otaPackage.setVersion("v1.0"); otaPackage.setFileName("filename.txt"); otaPackage.setContentType("text/plain"); @@ -933,6 +992,10 @@ public class VersionControlTest extends AbstractControllerTest { return otaPackageService.saveOtaPackage(otaPackage); } + private OtaPackage findOtaPackage(String title) throws Exception { + return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference>() {}, new PageLink(100, 0, title)).getData().get(0); + } + protected Dashboard createDashboard(CustomerId customerId, String name) { Dashboard dashboard = new Dashboard(); dashboard.setTitle(name); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java index 12c8e5053b..561b807b2b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java @@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService { OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); PageData findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); @@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService { void deleteOtaPackagesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 26376bbda3..9b4609dfef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.OtaPackageId; +import java.io.Serial; import java.nio.ByteBuffer; @Schema @@ -27,6 +28,7 @@ import java.nio.ByteBuffer; @EqualsAndHashCode(callSuper = true) public class OtaPackage extends OtaPackageInfo { + @Serial private static final long serialVersionUID = 3091601761339422546L; @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) @@ -44,4 +46,5 @@ public class OtaPackage extends OtaPackageInfo { super(otaPackage); this.data = otaPackage.getData(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index dfdbd83e80..138e7690e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -29,12 +31,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; + @Schema @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle { +public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle, ExportableEntity { + @Serial private static final long serialVersionUID = 3168391583570815419L; @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) @@ -77,6 +82,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp @Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY) private Long dataSize; + private OtaPackageId externalId; + public OtaPackageInfo() { super(); } @@ -100,6 +107,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm(); this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); + this.externalId = otaPackageInfo.getExternalId(); } @Schema(description = "JSON object with the ota package Id. " + @@ -128,9 +136,21 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } + @JsonProperty("otaVersion") + @JsonAlias("version") + public void setVersion(String version) { + this.version = version; + } + + @JsonProperty("otaVersion") + public String getVersion() { + return version; + } + @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 498fa5be3e..ba37067106 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; +import java.io.Serial; import java.util.Base64; import java.util.Optional; @@ -31,6 +32,7 @@ import java.util.Optional; @EqualsAndHashCode(callSuper = true) public class TbResource extends TbResourceInfo { + @Serial private static final long serialVersionUID = 7379609705527272306L; private byte[] data; @@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo { public String toString() { return super.toString(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index c763daeb7f..9d7187f7f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/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.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -58,7 +59,8 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "OTA_PACKAGE", value = OtaPackage.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 1ffafd305e..6ee4bdd89f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.nio.ByteBuffer; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CONTENT_TYPE_COLUMN; @@ -105,6 +106,9 @@ public class OtaPackageEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = EXTERNAL_ID_PROPERTY) + private UUID externalId; + public OtaPackageEntity() { super(); } @@ -128,6 +132,7 @@ public class OtaPackageEntity extends BaseSqlEntity { this.data = otaPackage.getData().array(); this.dataSize = otaPackage.getDataSize(); this.additionalInfo = otaPackage.getAdditionalInfo(); + this.externalId = getUuid(otaPackage.getExternalId()); } @Override @@ -153,6 +158,8 @@ public class OtaPackageEntity extends BaseSqlEntity { otaPackage.setHasData(true); } otaPackage.setAdditionalInfo(additionalInfo); + otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackage; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index c1e83cf511..a1625f2595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + @Transient private boolean hasData; @@ -125,11 +128,12 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); this.additionalInfo = otaPackageInfo.getAdditionalInfo(); + this.externalId = getUuid(otaPackageInfo.getExternalId()); } public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, String tag, String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize, - Object additionalInfo, boolean hasData) { + Object additionalInfo, UUID externalId, boolean hasData) { this.id = id; this.createdTime = createdTime; this.tenantId = tenantId; @@ -146,6 +150,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.dataSize = dataSize; this.hasData = hasData; this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class); + this.externalId = externalId; } @Override @@ -168,6 +173,8 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { otaPackageInfo.setDataSize(dataSize); otaPackageInfo.setAdditionalInfo(additionalInfo); otaPackageInfo.setHasData(hasData); + otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackageInfo; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 16d8517b6d..bf52de037f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseOtaPackageService extends AbstractCachedEntityService implements OtaPackageService { + public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -73,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Hashing.md5(); + case SHA256 -> Hashing.sha256(); + case SHA384 -> Hashing.sha384(); + case SHA512 -> Hashing.sha512(); + case CRC32 -> Hashing.crc32(); + case MURMUR3_32 -> Hashing.murmur3_32(); + case MURMUR3_128 -> Hashing.murmur3_128(); + default -> throw new DataValidationException("Unknown checksum algorithm!"); + }; } @Override @@ -171,6 +164,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } + @Override + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, type, title); + return otaPackageDao.findOtaPackageByTenantIdAndTitle(tenantId, type, title); + } + @Override public ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index f8f877e55e..de81f4e72d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -16,12 +16,17 @@ package org.thingsboard.server.dao.ota; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; -public interface OtaPackageDao extends Dao, TenantEntityWithDataDao { +public interface OtaPackageDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { Long sumDataSizeByTenantId(TenantId tenantId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index 780f67932e..da96b83d28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -22,7 +22,9 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -43,24 +45,52 @@ public class JpaOtaPackageDao extends JpaAbstractDao getEntityClass() { - return OtaPackageEntity.class; + public Long sumDataSizeByTenantId(TenantId tenantId) { + return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); } + @Transactional @Override - protected JpaRepository getRepository() { - return otaPackageRepository; + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTypeAndTitle(tenantId.getId(), type, title)); } + @Transactional @Override - public Long sumDataSizeByTenantId(TenantId tenantId) { - return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); } @Transactional @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(otaPackageRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)).map(OtaPackageId::new)); + } + + @Transactional + @Override + public OtaPackage findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) { + return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new); + } + + @Override + protected Class getEntityClass() { + return OtaPackageEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return otaPackageRepository; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index fe7e2bf015..c5ca894a00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; import java.util.UUID; public interface OtaPackageInfoRepository extends JpaRepository { - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND (:searchText IS NULL OR ilike(f.title, CONCAT('%', :searchText, '%')) = true)") Page findAllByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, true) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND f.deviceProfileId = :deviceProfileId " + "AND f.type = :type " + @@ -45,7 +46,7 @@ public interface OtaPackageInfoRepository extends JpaRepository { +public interface OtaPackageRepository extends JpaRepository, ExportableEntityRepository { @Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true) Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId); Page findByTenantId(UUID tenantId, Pageable pageable); + OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + + @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") + UUID getExternalIdById(@Param("id") UUID id); + + @Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index ad311f00df..0a6d6e578d 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -91,6 +91,8 @@ CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tena CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); +CREATE INDEX IF NOT EXISTS idx_ota_package_external_id ON ota_package(tenant_id, external_id); + CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index f2a0bc26c1..2cc8cbad0e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -216,7 +216,9 @@ CREATE TABLE IF NOT EXISTS ota_package ( data oid, data_size bigint, additional_info varchar, - CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) + external_id uuid, + CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version), + CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS queue ( From 7ffd468d0684b69a566acc1119053475e5466649 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:13:59 +0300 Subject: [PATCH 02/14] Improve OtaPackageExportData --- .../impl/OtaPackageExportService.java | 11 ++++-- .../impl/OtaPackageImportService.java | 15 ++------ .../server/common/data/OtaPackageInfo.java | 13 ------- .../common/data/sync/ie/EntityExportData.java | 3 +- .../data/sync/ie/OtaPackageExportData.java | 37 +++++++++++++++++++ .../thingsboard/common/util/JacksonUtil.java | 3 -- .../server/dao/sql/JpaAbstractDao.java | 3 -- .../server/dao/sql/asset/JpaAssetDao.java | 3 -- 8 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java index f9285e3699..1cfc70e39f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.OtaPackageId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -29,13 +29,18 @@ import java.util.Set; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageExportService extends BaseEntityExportService> { +public class OtaPackageExportService extends BaseEntityExportService { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData) { otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); } + @Override + protected OtaPackageExportData newExportData() { + return new OtaPackageExportData(); + } + @Override public Set getSupportedEntityTypes() { return Set.of(EntityType.OTA_PACKAGE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index b5c583e94d..d181a9bf0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,11 +19,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @@ -31,7 +29,7 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageImportService extends BaseEntityImportService> { +public class OtaPackageImportService extends BaseEntityImportService { private final OtaPackageService otaPackageService; @@ -41,7 +39,7 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) { otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); return otaPackage; } @@ -66,15 +64,10 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) { return otaPackageService.saveOtaPackage(otaPackage); } - @Override - protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { - super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); - } - @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 138e7690e3..2930ff41d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.common.data; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -136,17 +134,6 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } - @JsonProperty("otaVersion") - @JsonAlias("version") - public void setVersion(String version) { - this.version = version; - } - - @JsonProperty("otaVersion") - public String getVersion() { - return version; - } - @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index a1692aef04..5e1f98638a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -41,7 +41,8 @@ import java.util.Map; @Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), - @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) + @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class), + @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class) }) @JsonInclude(JsonInclude.Include.NON_NULL) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java new file mode 100644 index 0000000000..df12bb5b08 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 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.sync.ie; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.OtaPackage; + +@EqualsAndHashCode(callSuper = true) +public class OtaPackageExportData extends EntityExportData { + + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public OtaPackage getEntity() { + return super.getEntity(); + } + + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public void setEntity(OtaPackage entity) { + super.setEntity(entity); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index d153501b92..cd61c7ac20 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -62,9 +62,6 @@ import java.util.function.BiFunction; import java.util.function.UnaryOperator; import java.util.regex.Pattern; -/** - * Created by Valerii Sosliuk on 5/12/2017. - */ @Slf4j public class JacksonUtil { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 05577b68e1..6f0380752f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -40,9 +40,6 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -/** - * @author Valerii Sosliuk - */ @Slf4j @SqlDao public abstract class JpaAbstractDao, D> diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 4e99fb57e4..4b55884792 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -48,9 +48,6 @@ import java.util.UUID; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; -/** - * Created by Valerii Sosliuk on 5/19/2017. - */ @Component @SqlDao @Slf4j From 562b23aef103092d69f1c5025ea713d257c4c8e2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:50:35 +0300 Subject: [PATCH 03/14] Minor clean up --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index d181a9bf0a..896724c827 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -58,11 +58,6 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Wed, 11 Jun 2025 16:09:34 +0300 Subject: [PATCH 04/14] Improve findOtaByName in VC --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../org/thingsboard/server/dao/ota/OtaPackageService.java | 2 +- .../thingsboard/server/dao/ota/BaseOtaPackageService.java | 6 +++--- .../java/org/thingsboard/server/dao/ota/OtaPackageDao.java | 2 +- .../thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java | 5 ++--- .../server/dao/sql/ota/OtaPackageRepository.java | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 896724c827..16b204411e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -48,7 +48,7 @@ public class OtaPackageImportService extends BaseEntityImportService findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index bf52de037f..83cb35d152 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -165,9 +165,9 @@ public class BaseOtaPackageService extends AbstractCachedEntityService, TenantEntityWithDataDao, Long sumDataSizeByTenantId(TenantId tenantId); - OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index da96b83d28..7322ea4cb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -51,8 +50,8 @@ public class JpaOtaPackageDao extends JpaAbstractDao findByTenantId(UUID tenantId, Pageable pageable); - OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version); @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); From fbb7d5c415b734834ba4bd9caf8031a7930f7644 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 12 Jun 2025 16:46:02 +0300 Subject: [PATCH 05/14] Minor improvement to correctly save ota if using the same device --- .../sync/ie/importing/impl/DeviceProfileImportService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index e66d2d1267..c867286285 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -45,8 +45,8 @@ public class DeviceProfileImportService extends BaseEntityImportService Date: Thu, 19 Jun 2025 17:09:08 +0300 Subject: [PATCH 06/14] Refactoring --- .../impl/DeviceProfileImportService.java | 10 +++++---- .../server/common/data/id/OtaPackageId.java | 2 ++ .../create/AutoVersionCreateConfig.java | 3 +++ .../server/dao/ota/BaseOtaPackageService.java | 22 ++++++++----------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index c867286285..e498cf0911 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -52,12 +52,14 @@ public class DeviceProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds(); + if (toUpdate) { + deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); + deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); + } DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); - if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + if (toUpdate) { importCalculatedFields(ctx, saved, exportData, idProvider); - saved.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); - saved.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); - saved = deviceProfileService.saveDeviceProfile(saved); } return saved; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 2a8efc5bfb..a246ab5f94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class OtaPackageId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java index 8e61991b38..b5e2813134 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import lombok.Data; import lombok.EqualsAndHashCode; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @Data public class AutoVersionCreateConfig extends VersionCreateConfig { + @Serial private static final long serialVersionUID = 8245450889383315551L; private String branch; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 83cb35d152..7aff945e84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Thu, 19 Jun 2025 17:15:19 +0300 Subject: [PATCH 07/14] Minor improvement of saveOtaPackageInfo --- .../org/thingsboard/server/dao/ota/BaseOtaPackageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 7aff945e84..343a2485ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Fri, 20 Jun 2025 15:15:15 +0300 Subject: [PATCH 08/14] Fix ota save when saving url --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 +++++ .../dao/service/validator/OtaPackageDataValidator.java | 1 + 2 files changed, 6 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 16b204411e..fd8a7822eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,6 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData; @@ -60,6 +61,10 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Fri, 20 Jun 2025 15:54:01 +0300 Subject: [PATCH 09/14] Fix error on import --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../java/org/thingsboard/server/common/data/OtaPackage.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index fd8a7822eb..9cd6bb50c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -63,7 +63,7 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Mon, 23 Jun 2025 11:45:41 +0300 Subject: [PATCH 10/14] Add name to OtaPackageInfo json --- .../org/thingsboard/server/common/data/OtaPackageInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 2930ff41d1..9f010db823 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -124,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp } @Override - @JsonIgnore + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; } From eb1e3dfabfe725f8fb80bccff1b4d10f9348d983 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jun 2025 12:28:46 +0300 Subject: [PATCH 11/14] UI: Version Control for OTA updates --- .../modules/home/pages/admin/admin.module.ts | 2 + .../resource-library-tabs.component.html | 23 ++++++++++ .../resource-library-tabs.component.ts | 36 +++++++++++++++ .../resources-library-table-config.resolve.ts | 2 + .../ota-update-table-config.resolve.ts | 2 + .../ota-update/ota-update-tabs.component.html | 23 ++++++++++ .../ota-update/ota-update-tabs.component.ts | 44 +++++++++++++++++++ .../pages/ota-update/ota-update.module.ts | 4 +- .../app/shared/models/entity-type.models.ts | 2 + .../app/shared/models/ota-package.models.ts | 4 +- ui-ngx/src/app/shared/models/vc.models.ts | 6 ++- .../assets/locale/locale.constant-en_US.json | 2 + 12 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index a5f18122fd..10721ade5a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -38,6 +38,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; import { NgxFlowModule } from '@flowjs/ngx-flow'; import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @NgModule({ declarations: @@ -50,6 +51,7 @@ import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.compo HomeSettingsComponent, ResourcesLibraryComponent, ResourceTabsComponent, + ResourceLibraryTabsComponent, ResourcesTableHeaderComponent, JsResourceComponent, JsLibraryTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html new file mode 100644 index 0000000000..4effdaad53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts new file mode 100644 index 0000000000..a85bf48db3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2025 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. +/// + +import { Component } from '@angular/core'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { Resource } from '@shared/models/resource.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-resource-library-tabs', + templateUrl: './resource-library-tabs.component.html', + styleUrls: [] +}) +export class ResourceLibraryTabsComponent extends EntityTabsComponent { + + readonly NULL_UUID = NULL_UUID; + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 91327b7465..f39ed9ff7b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -37,6 +37,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -55,6 +56,7 @@ export class ResourcesLibraryTableConfigResolver { this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.headerComponent = ResourcesTableHeaderComponent; + this.config.entityTabsComponent = ResourceLibraryTabsComponent; this.config.entityTitle = (resource) => resource ? resource.title : ''; diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index cc3ca4046b..0dae778d03 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -36,6 +36,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @Injectable() export class OtaUpdateTableConfigResolve { @@ -50,6 +51,7 @@ export class OtaUpdateTableConfigResolve { private fileSize: FileSizePipe) { this.config.entityType = EntityType.OTA_PACKAGE; this.config.entityComponent = OtaUpdateComponent; + this.config.entityTabsComponent = OtaUpdateTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE); this.config.entityResources = entityTypeResources.get(EntityType.OTA_PACKAGE); diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html new file mode 100644 index 0000000000..a8cdae4256 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts new file mode 100644 index 0000000000..1de3031ccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2025 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. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { OtaPackage } from '@shared/models/ota-package.models'; + +@Component({ + selector: 'tb-ota-update-tabs', + templateUrl: './ota-update-tabs.component.html', + styleUrls: [] +}) +export class OtaUpdateTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + isTenantOtaUpdate() { + return this.entity && this.entity.tenantId.id !== NULL_UUID; + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts index 139591f14f..fe24da31b2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts @@ -20,10 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { OtaUpdateRoutingModule } from '@home/pages/ota-update/ota-update-routing.module'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @NgModule({ declarations: [ - OtaUpdateComponent + OtaUpdateComponent, + OtaUpdateTabsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48cad42e7b..60e2956e7c 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -346,6 +346,8 @@ export const entityTypeTranslations = new Map, HasTenantId { +export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 3795518ffc..ebd7840f61 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -33,16 +33,18 @@ export const exportableEntityTypes: Array = [ EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ]; -export const entityTypesWithoutRelatedData: Set = new Set([ +export const entityTypesWithoutRelatedData = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.TB_RESOURCE + EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, ]); export interface VersionCreateConfig { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 55e136e031..a63089bb6c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2584,6 +2584,8 @@ "type-tb-resources": "Resources", "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "type-ota-package": "OTA package", + "type-ota-packages": "OTA packages", + "list-of-ota-packages": "{ count, plural, =1 {One OTA package} other {List of # OTA packages} }", "type-rpc": "RPC", "type-queue": "Queue", "type-queue-stats": "Queue statistics", From ac6508986c38e3c55fc65a0eec548474c2b32dec Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jun 2025 17:28:16 +0300 Subject: [PATCH 12/14] UI: Clear code and omit label --- .../home/pages/ota-update/ota-update-tabs.component.ts | 4 ---- ui-ngx/src/app/shared/models/ota-package.models.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts index 1de3031ccb..44d17f3c11 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -37,8 +37,4 @@ export class OtaUpdateTabsComponent extends EntityTabsComponent { return this.entity && this.entity.tenantId.id !== NULL_UUID; } - ngOnInit() { - super.ngOnInit(); - } - } diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index 234886063e..bba7ea3d04 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -86,7 +86,7 @@ export interface OtaPagesIds { softwareId?: OtaPackageId; } -export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { +export interface OtaPackageInfo extends Omit, 'label'>, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; From 5949fa1ac81a24a6c1acdf2db0a1f3e7c2975f3e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 23 Jun 2025 18:01:26 +0300 Subject: [PATCH 13/14] Small refactoring of OtaPackageController --- .../controller/OtaPackageController.java | 35 +++++++------------ .../ota/DefaultTbOtaPackageService.java | 1 + 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 43ebf89b41..d4f932a643 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; 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.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.OtaPackage; @@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.io.IOException; - import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; @@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController { @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}/download") public ResponseEntity downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/info/{otaPackageId}") public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}") public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController { "Referencing non-existing OTA Package Id will cause 'Not Found' error. " + "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/otaPackage") public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.") - @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { + @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception { otaPackageInfo.setTenantId(getTenantId()); checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE); @@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController { notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE) - @ResponseBody + @PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE) public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'") @@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController { @Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"})) @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @Parameter(description = "OTA Package data.") - @RequestPart MultipartFile file) throws ThingsboardException, IOException { + @RequestPart MultipartFile file) throws Exception { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages") public PageData getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("deviceProfileId") String strDeviceProfileId, @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController { notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/otaPackage/{otaPackageId}") public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index af8bbeb669..2d597f7053 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen throw e; } } + } From 71ec0a588a402faa0a644cd5a7e5c617b354c56f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 11:34:17 +0300 Subject: [PATCH 14/14] Fix test to use profile with ota on import --- .../service/sync/vc/VersionControlTest.java | 25 +++++++++++-------- .../data/sync/ie/OtaPackageExportData.java | 4 +++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 5c5b7bb107..461ca5a2ec 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -685,23 +685,28 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testOtaPackageVc_betweenTenants() throws Exception { + public void testOtaPackageVcWithProfile_betweenTenants() throws Exception { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); + deviceProfile.setFirmwareId(firmware.getId()); + deviceProfile.setSoftwareId(software.getId()); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); - OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); - OtaPackage softwareOta = findOtaPackage(software.getTitle()); - loginTenant2(); loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); - OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); - OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); - checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); - checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); - checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); - checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName()); + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile); + checkImportedDeviceProfileData(deviceProfile, importedProfile); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); + assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId()); + assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId()); } protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java index df12bb5b08..44e2f7857c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -22,6 +22,10 @@ import org.thingsboard.server.common.data.OtaPackage; @EqualsAndHashCode(callSuper = true) public class OtaPackageExportData extends EntityExportData { + /* + * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) + * We override both methods to ensure 'version' is not ignored during (de)serialization. + */ @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) @Override public OtaPackage getEntity() {