From 2b51451896f74aad4854bd199b3abd41dc4d0fa3 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 28 Feb 2025 18:17:20 +0200 Subject: [PATCH] Change cf strategy for vc --- .../DefaultEntitiesExportImportService.java | 13 +- .../sync/ie/EntitiesExportImportService.java | 1 - .../ie/exporting/impl/AssetExportService.java | 17 ++- .../impl/AssetProfileExportService.java | 17 ++- .../BaseCalculatedFieldsExportService.java | 51 ++++++++ .../impl/CalculatedFieldExportService.java | 49 ------- .../exporting/impl/DeviceExportService.java | 11 +- .../impl/DeviceProfileExportService.java | 17 ++- .../ie/importing/impl/AssetImportService.java | 18 ++- .../impl/AssetProfileImportService.java | 18 ++- .../BaseCalculatedFieldsImportService.java | 123 ++++++++++++++++++ .../impl/CalculatedFieldImportService.java | 85 ------------ .../importing/impl/DeviceImportService.java | 48 ++++--- .../impl/DeviceProfileImportService.java | 18 ++- .../DefaultEntitiesVersionControlService.java | 10 +- .../sync/vc/data/EntitiesExportCtx.java | 2 + .../sync/vc/data/EntitiesImportCtx.java | 12 +- .../sync/vc/data/EntityTypeExportCtx.java | 1 + .../sync/ie/ExportImportServiceSqlTest.java | 50 ++++--- .../service/sync/vc/VersionControlTest.java | 11 +- .../common/data/sync/ie/AssetExportData.java | 28 ++++ .../data/sync/ie/AssetProfileExportData.java | 28 ++++ .../sync/ie/CalculatedFieldExportData.java | 56 ++++++++ .../common/data/sync/ie/DeviceExportData.java | 3 +- .../data/sync/ie/DeviceProfileExportData.java | 28 ++++ .../common/data/sync/ie/EntityExportData.java | 8 ++ .../data/sync/ie/EntityExportSettings.java | 3 + .../data/sync/ie/EntityImportSettings.java | 3 + .../request/create/VersionCreateConfig.java | 5 + .../vc/request/load/VersionLoadConfig.java | 1 + .../dao/cf/BaseCalculatedFieldService.java | 1 - .../model/sql/CalculatedFieldLinkEntity.java | 11 +- 32 files changed, 525 insertions(+), 222 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java 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 7d0f5ebf48..94b0ddc07e 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 @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final Map> importServices = new HashMap<>(); private final RelationService relationService; + private final CalculatedFieldService calculatedFieldService; private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; @@ -69,11 +71,9 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, - EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.CALCULATED_FIELD + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ); - @Override public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx ctx, I entityId) throws ThingsboardException { if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) { @@ -128,15 +128,18 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS logEntityActionService.logEntityRelationAction(ctx.getTenantId(), null, relation, ctx.getUser(), ActionType.RELATION_ADD_OR_UPDATE, null, relation); } - } + ctx.getCalculatedFields().forEach((calculatedField, created) -> { + var savedCalculatedField = calculatedFieldService.save(calculatedField); + logEntityActionService.logEntityAction(ctx.getTenantId(), savedCalculatedField.getId(), savedCalculatedField, created ? ActionType.ADDED : ActionType.UPDATED, ctx.getUser()); + }); + } @Override public Comparator getEntityTypeComparatorForImport() { return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf); } - @SuppressWarnings("unchecked") private , D extends EntityExportData> EntityExportService getExportService(EntityType entityType) { EntityExportService exportService = exportServices.get(entityType); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java index 2b7e7d3593..b77447df8d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java @@ -32,7 +32,6 @@ public interface EntitiesExportImportService { , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException; - void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException; Comparator getEntityTypeComparatorForImport(); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index 56d5bb0934..6f56fa5145 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -19,7 +19,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -27,12 +28,22 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetExportService extends BaseEntityExportService> { +public class AssetExportService extends BaseCalculatedFieldsExportService { + + protected AssetExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, Asset asset, AssetExportData exportData) { asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId())); asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId())); + setCalculatedFields(ctx, asset, exportData); + } + + @Override + protected AssetExportData newExportData() { + return new AssetExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java index b670daf06c..2314db92f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java @@ -19,7 +19,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.AssetProfileId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.common.data.sync.ie.AssetProfileExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -27,13 +28,23 @@ import java.util.Set; @Service @TbCoreComponent -public class AssetProfileExportService extends BaseEntityExportService> { +public class AssetProfileExportService extends BaseCalculatedFieldsExportService { + + protected AssetProfileExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData) { assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId())); assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultEdgeRuleChainId())); + setCalculatedFields(ctx, assetProfile, exportData); + } + + @Override + protected AssetProfileExportData newExportData() { + return new AssetProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java new file mode 100644 index 0000000000..e28f8aaa79 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseCalculatedFieldsExportService.java @@ -0,0 +1,51 @@ +/** + * 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.service.sync.ie.exporting.impl; + +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.List; + +public abstract class BaseCalculatedFieldsExportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityExportService { + + protected final CalculatedFieldService calculatedFieldService; + + protected BaseCalculatedFieldsExportService(CalculatedFieldService calculatedFieldService) { + this.calculatedFieldService = calculatedFieldService; + } + + protected void setCalculatedFields(EntitiesExportCtx ctx, E entity, D exportData) { + if (ctx.getSettings().isExportCalculatedFields()) { + List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entity.getId()); + calculatedFields.forEach(calculatedField -> { + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + EntityId externalId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); + argument.setRefEntityId(externalId); + } + }); + }); + exportData.setCalculatedFields(calculatedFields); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java deleted file mode 100644 index b42f3c6647..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/CalculatedFieldExportService.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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.service.sync.ie.exporting.impl; - -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; - -import java.util.Set; - -@Service -@TbCoreComponent -public class CalculatedFieldExportService extends BaseEntityExportService> { - - @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, CalculatedField calculatedField, EntityExportData exportData) { - calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, calculatedField.getEntityId())); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - EntityId internalEntityId = getExternalIdOrElseInternal(ctx, argument.getRefEntityId()); - argument.setRefEntityId(internalEntityId); - } - }); - } - - @Override - public Set getSupportedEntityTypes() { - return Set.of(EntityType.CALCULATED_FIELD); - } - -} 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..f1b5ade487 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 @@ -15,12 +15,12 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; @@ -29,11 +29,15 @@ import java.util.Set; @Service @TbCoreComponent -@RequiredArgsConstructor -public class DeviceExportService extends BaseEntityExportService { +public class DeviceExportService extends BaseCalculatedFieldsExportService { private final DeviceCredentialsService deviceCredentialsService; + public DeviceExportService(CalculatedFieldService calculatedFieldService, DeviceCredentialsService deviceCredentialsService) { + super(calculatedFieldService); + this.deviceCredentialsService = deviceCredentialsService; + } + @Override protected void setRelatedEntities(EntitiesExportCtx ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); @@ -44,6 +48,7 @@ public class DeviceExportService extends BaseEntityExportService> { +public class DeviceProfileExportService extends BaseCalculatedFieldsExportService { + + protected DeviceProfileExportService(CalculatedFieldService calculatedFieldService) { + super(calculatedFieldService); + } @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData) { + protected void setRelatedEntities(EntitiesExportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData) { deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId())); + setCalculatedFields(ctx, deviceProfile, exportData); + } + + @Override + protected DeviceProfileExportData newExportData() { + return new DeviceProfileExportData(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 00ce23b2e8..0b0649072e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -15,24 +15,28 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; 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.AssetExportData; import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class AssetImportService extends BaseEntityImportService> { +public class AssetImportService extends BaseCalculatedFieldsImportService { private final AssetService assetService; + public AssetImportService(CalculatedFieldService calculatedFieldService, AssetService assetService) { + super(calculatedFieldService); + this.assetService = assetService; + } + @Override protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) { asset.setTenantId(tenantId); @@ -40,14 +44,14 @@ public class AssetImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, AssetExportData exportData, IdProvider idProvider) { asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId())); return asset; } @Override - protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider) { - return assetService.saveAsset(asset); + protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, AssetExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, asset, exportData, idProvider, assetService::saveAsset); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 97c54957bf..6797c92c81 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -23,25 +22,30 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AssetProfileId; 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.AssetProfileExportData; import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class AssetProfileImportService extends BaseEntityImportService> { +public class AssetProfileImportService extends BaseCalculatedFieldsImportService { private final AssetProfileService assetProfileService; + public AssetProfileImportService(CalculatedFieldService calculatedFieldService, AssetProfileService assetProfileService) { + super(calculatedFieldService); + this.assetProfileService = assetProfileService; + } + @Override protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) { assetProfile.setTenantId(tenantId); } @Override - protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData exportData, IdProvider idProvider) { + protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, AssetProfileExportData exportData, IdProvider idProvider) { assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId())); assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId())); assetProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(assetProfile.getDefaultEdgeRuleChainId())); @@ -49,8 +53,8 @@ public class AssetProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) { - return assetProfileService.saveAssetProfile(assetProfile); + protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfileExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, assetProfile, exportData, idProvider, assetProfileService::saveAssetProfile); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java new file mode 100644 index 0000000000..ffa51938f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseCalculatedFieldsImportService.java @@ -0,0 +1,123 @@ +/** + * 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.service.sync.ie.importing.impl; + +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.sync.ie.CalculatedFieldExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class BaseCalculatedFieldsImportService & HasTenantId, D extends CalculatedFieldExportData> extends BaseEntityImportService { + + private final CalculatedFieldService calculatedFieldService; + + protected BaseCalculatedFieldsImportService(CalculatedFieldService calculatedFieldService) { + this.calculatedFieldService = calculatedFieldService; + } + + protected E saveOrUpdateEntity(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, Function saveFunction) { + E savedEntity = saveFunction.apply(entity); + + if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + saveCalculatedFields(ctx, savedEntity, exportData, idProvider); + } + return savedEntity; + } + + protected void saveCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) { + if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) { + return; + } + + exportData.getCalculatedFields().forEach(calculatedField -> { + calculatedField.setTenantId(savedEntity.getTenantId()); + calculatedField.setExternalId(calculatedField.getId()); + calculatedField.setId(idProvider.getInternalId(calculatedField.getId(), false)); + calculatedField.setEntityId(savedEntity.getId()); + + calculatedField.getConfiguration().getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + } + }); + + calculatedFieldService.save(calculatedField); + }); + } + + @Override + protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { + boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider); + updated |= updateCalculatedFields(ctx, prepared, exportData, idProvider); + return updated; + } + + private boolean updateCalculatedFields(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { + var calculatedFields = exportData.getCalculatedFields(); + if (calculatedFields == null || !ctx.isSaveCalculatedFields()) { + return false; + } + Map calculatedFieldMap = calculatedFields.stream() + .peek(newField -> { + newField.setTenantId(ctx.getTenantId()); + newField.setExternalId(newField.getId()); + newField.setId(idProvider.getInternalId(newField.getId(), false)); + newField.setEntityId(prepared.getId()); + newField.getConfiguration().getArguments().values().forEach(argument -> { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), false)); + }); + }) + .collect(Collectors.toMap(CalculatedField::getId, field -> field)); + + List existingFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), prepared.getId()); + boolean updated = false; + + Map result = new LinkedHashMap<>(); + for (CalculatedField existingField : existingFields) { + if (calculatedFieldMap.containsKey(existingField.getId())) { + CalculatedField newField = calculatedFieldMap.get(existingField.getId()); + if (!newField.equals(existingField)) { + result.put(newField, false); + } + calculatedFieldMap.remove(existingField.getId()); + } else { + updated = true; + calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId()); + } + } + + for (CalculatedField newField : calculatedFieldMap.values()) { + result.put(newField, true); + } + + if (!result.isEmpty()) { + updated = true; + ctx.addCalculatedFields(result); + } + return updated; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java deleted file mode 100644 index a723f28b1b..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CalculatedFieldImportService.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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.service.sync.ie.importing.impl; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; -import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.service.ConstraintValidator; -import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; - -@Service -@TbCoreComponent -@RequiredArgsConstructor -public class CalculatedFieldImportService extends BaseEntityImportService> { - - private final CalculatedFieldService calculatedFieldService; - - @Override - protected void setOwner(TenantId tenantId, CalculatedField calculatedField, IdProvider idProvider) { - calculatedField.setTenantId(tenantId); - } - - @Override - protected CalculatedField prepare(EntitiesImportCtx ctx, CalculatedField calculatedField, CalculatedField oldEntity, EntityExportData exportData, IdProvider idProvider) { - calculatedField.setEntityId(idProvider.getInternalId(calculatedField.getEntityId())); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - EntityId internalEntityId = idProvider.getInternalId(argument.getRefEntityId()); - argument.setRefEntityId(internalEntityId); - } - }); - return calculatedField; - } - - @Override - protected CalculatedField saveOrUpdate(EntitiesImportCtx ctx, CalculatedField calculatedField, EntityExportData exportData, IdProvider idProvider) { - ConstraintValidator.validateFields(calculatedField); - return calculatedFieldService.save(calculatedField); - } - - @Override - protected CalculatedField deepCopy(CalculatedField calculatedField) { - return new CalculatedField(calculatedField); - } - - @Override - protected void onEntitySaved(User user, CalculatedField savedEntity, CalculatedField oldEntity) throws ThingsboardException { - entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity, null, - oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null); - } - - @Override - protected void cleanupForComparison(CalculatedField e) { - super.cleanupForComparison(e); - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD; - } - -} 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 61b9839cbd..c8e99cdbc1 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 @@ -15,13 +15,13 @@ */ package org.thingsboard.server.service.sync.ie.importing.impl; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.DeviceExportData; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -29,12 +29,17 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent -@RequiredArgsConstructor -public class DeviceImportService extends BaseEntityImportService { +public class DeviceImportService extends BaseCalculatedFieldsImportService { private final DeviceService deviceService; private final DeviceCredentialsService credentialsService; + public DeviceImportService(CalculatedFieldService calculatedFieldService, DeviceService deviceService, DeviceCredentialsService credentialsService) { + super(calculatedFieldService); + this.deviceService = deviceService; + this.credentialsService = credentialsService; + } + @Override protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) { device.setTenantId(tenantId); @@ -64,31 +69,44 @@ public class DeviceImportService extends BaseEntityImportService> { +public class DeviceProfileImportService extends BaseCalculatedFieldsImportService { private final DeviceProfileService deviceProfileService; + public DeviceProfileImportService(CalculatedFieldService calculatedFieldService, DeviceProfileService deviceProfileService) { + super(calculatedFieldService); + this.deviceProfileService = deviceProfileService; + } + @Override protected void setOwner(TenantId tenantId, DeviceProfile deviceProfile, IdProvider idProvider) { deviceProfile.setTenantId(tenantId); } @Override - protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, EntityExportData exportData, IdProvider idProvider) { + protected DeviceProfile prepare(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfile old, DeviceProfileExportData exportData, IdProvider idProvider) { deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId())); @@ -51,8 +55,8 @@ public class DeviceProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) { - return deviceProfileService.saveDeviceProfile(deviceProfile); + protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, DeviceProfileExportData exportData, IdProvider idProvider) { + return saveOrUpdateEntity(ctx, deviceProfile, exportData, idProvider, deviceProfileService::saveDeviceProfile); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index eed18abe52..89b51ce84a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -94,7 +94,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.function.Function; -import java.util.stream.Collectors; import static com.google.common.util.concurrent.Futures.transform; import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName; @@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(false) .build()); ctx.setFinalImportAttempt(true); @@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont var sw = TbStopWatch.create("before"); List entityTypes = request.getEntityTypes().keySet().stream() - .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList()); + .sorted(exportImportService.getEntityTypeComparatorForImport()).toList(); for (EntityType entityType : entityTypes) { log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType); sw.startNew("Entities " + entityType.name()); @@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .updateRelations(config.isLoadRelations()) .saveAttributes(config.isLoadAttributes()) .saveCredentials(config.isLoadCredentials()) + .saveCalculatedFields(config.isLoadCalculatedFields()) .findExistingByName(config.isFindExistingEntityByName()) .build(); } @@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception { + public ListenableFuture compareEntityDataToVersion(User user, EntityId entityId, String versionId) { HasId entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId); if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type"); @@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont .exportRelations(otherVersion.hasRelations()) .exportAttributes(otherVersion.hasAttributes()) .exportCredentials(otherVersion.hasCredentials()) + .exportCalculatedFields(otherVersion.hasCalculatedFields()) .build()); EntityExportData currentVersion; try { @@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont @Override - public ListenableFuture> listBranches(TenantId tenantId) throws Exception { + public ListenableFuture> listBranches(TenantId tenantId) { return gitServiceQueue.listBranches(tenantId); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java index 669dad347a..b3007c6738 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesExportCtx.java @@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx { .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); } @@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx { log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId); externalIdMap.put(internalId, externalId != null ? externalId : internalId); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java index ab836fa13d..c1a8dad161 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntitiesImportCtx.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -31,6 +32,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -54,6 +56,7 @@ public class EntitiesImportCtx { private final Set notFoundIds = new HashSet<>(); private final Set relations = new LinkedHashSet<>(); + private final Map calculatedFields = new LinkedHashMap<>(); private boolean finalImportAttempt = false; private EntityImportSettings settings; @@ -91,6 +94,10 @@ public class EntitiesImportCtx { return getSettings().isSaveCredentials(); } + public boolean isSaveCalculatedFields() { + return getSettings().isSaveCalculatedFields(); + } + public EntityId getInternalId(EntityId externalId) { var result = externalToInternalIdMap.get(externalId); log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss"); @@ -120,6 +127,10 @@ public class EntitiesImportCtx { relations.addAll(values); } + public void addCalculatedFields(Map calculatedFieldMap) { + calculatedFields.putAll(calculatedFieldMap); + } + public void addReferenceCallback(EntityId externalId, ThrowingRunnable tr) { if (tr != null) { referenceCallbacks.put(externalId, tr); @@ -140,5 +151,4 @@ public class EntitiesImportCtx { return notFoundIds.contains(externalId); } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java index e0a5b40281..55c8a763b0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/data/EntityTypeExportCtx.java @@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx .exportRelations(config.isSaveRelations()) .exportAttributes(config.isSaveAttributes()) .exportCredentials(config.isSaveCredentials()) + .exportCalculatedFields(config.isSaveCalculatedFields()) .build(); this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE; } 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 8870194685..9184a3c034 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 @@ -80,6 +80,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.sync.ie.DeviceExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; @@ -207,11 +208,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { 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(), calculatedField.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() .exportCredentials(false) + .exportCalculatedFields(true) .build()); } catch (Exception e) { throw new RuntimeException(e); @@ -281,23 +283,28 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + // calculated field of imported device: + List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); + assertThat(calculatedFields.size()).isOne(); + var importedCalculatedField = calculatedFields.get(0); + assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); + EntityExportData updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE); updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel()); Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity(); verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice)); - CalculatedField importedCalculatedField = (CalculatedField) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), - any(), eq(ActionType.ADDED), isNull()); - importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD)); - verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedCalculatedField.getId()), eq(importedCalculatedField), - any(), eq(ActionType.UPDATED), isNull()); + // update calculated field: + DeviceExportData deviceExportData = (DeviceExportData) getAndClone(entitiesExportData, EntityType.DEVICE); + deviceExportData.setCalculatedFields(deviceExportData.getCalculatedFields().stream().peek(field -> field.setName("t_" + field.getName())).toList()); + importEntity(tenantAdmin2, deviceExportData).getSavedEntity(); - EntityExportData updatedCalculatedFieldEntity = getAndClone(entitiesExportData, EntityType.CALCULATED_FIELD); - updatedCalculatedFieldEntity.getEntity().setName("t" + updatedCalculatedFieldEntity.getEntity().getName()); - CalculatedField updatedCalculatedField = importEntity(tenantAdmin2, updatedCalculatedFieldEntity).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(updatedCalculatedField.getId()), eq(updatedCalculatedField), - any(), eq(ActionType.UPDATED), isNull()); + calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); + assertThat(calculatedFields.size()).isOne(); + importedCalculatedField = calculatedFields.get(0); + assertThat(importedCalculatedField.getExternalId()).isEqualTo(calculatedField.getId()); + assertThat(importedCalculatedField.getName()).startsWith("t_"); } @Test @@ -320,10 +327,11 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { 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(), calculatedField.getId())) { + deviceProfile.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) + .saveCalculatedFields(true) .build()); ids.put(entityId, (EntityId) importResult.getSavedEntity().getId()); } @@ -353,19 +361,20 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity(); + 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()); + List calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + assertThat(calculatedFields.size()).isOne(); + CalculatedField field = calculatedFields.get(0); + assertThat(field.getName()).isEqualTo(calculatedField.getName()); + EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity(); assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId()); - CalculatedField exportedCalculatedField = (CalculatedField) exportEntity(tenantAdmin2, (CalculatedFieldId) ids.get(calculatedField.getId())).getEntity(); - assertThat(exportedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(exportedCalculatedField.getEntityId()).isEqualTo(device.getId()); - assertThat(exportedCalculatedField.getConfiguration().getReferencedEntities()).isEqualTo(calculatedField.getConfiguration().getReferencedEntities()); - deviceProfile.setDefaultDashboardId(null); deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile importedDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId2, (DeviceProfileId) ids.get(deviceProfile.getId())); @@ -373,7 +382,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { Device device = new Device(); device.setTenantId(tenantId); @@ -618,6 +626,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected , I extends EntityId> EntityExportData exportEntity(User user, I entityId) throws Exception { return exportEntity(user, entityId, EntityExportSettings.builder() .exportCredentials(true) + .exportCalculatedFields(true) .build()); } @@ -628,6 +637,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { protected , I extends EntityId> EntityImportResult importEntity(User user, EntityExportData exportData) throws Exception { return importEntity(user, exportData, EntityImportSettings.builder() .saveCredentials(true) + .saveCalculatedFields(true) .build()); } 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 28eaf06da7..453fea676f 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 @@ -576,12 +576,12 @@ public class VersionControlTest extends AbstractControllerTest { Asset asset = createAsset(null, null, "Asset 1"); Device device = createDevice(null, null, "Device 1", "test1"); CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); - String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD); + String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); loginTenant2(); loadVersion(versionId, config -> { config.setLoadCredentials(false); - }, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CALCULATED_FIELD); + }, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); Asset importedAsset = findAsset(asset.getName()); Device importedDevice = findDevice(device.getName()); @@ -602,9 +602,9 @@ public class VersionControlTest extends AbstractControllerTest { public void testVcWithCalculatedFields_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); CalculatedField calculatedField = createCalculatedField("CalculatedField", asset.getId(), asset.getId()); - String versionId = createVersion("asset and field", EntityType.ASSET, EntityType.CALCULATED_FIELD); + String versionId = createVersion("asset and field", EntityType.ASSET); - loadVersion(versionId, EntityType.ASSET, EntityType.CALCULATED_FIELD); + loadVersion(versionId, EntityType.ASSET); CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId()); checkImportedEntity(tenantId1, calculatedField, tenantId1, importedCalculatedField); assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName()); @@ -676,10 +676,10 @@ public class VersionControlTest extends AbstractControllerTest { request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> { EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig(); config.setAllEntities(true); - config.setSaveRelations(true); config.setSaveAttributes(true); config.setSaveCredentials(true); + config.setSaveCalculatedFields(true); return config; }))); @@ -745,6 +745,7 @@ public class VersionControlTest extends AbstractControllerTest { config.setLoadAttributes(true); config.setLoadRelations(true); config.setLoadCredentials(true); + config.setLoadCalculatedFields(true); config.setRemoveOtherEntities(false); config.setFindExistingEntityByName(true); configModifier.accept(config); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java new file mode 100644 index 0000000000..4bb836d5d9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetExportData.java @@ -0,0 +1,28 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.asset.Asset; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class AssetExportData extends CalculatedFieldExportData { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java new file mode 100644 index 0000000000..42d9523828 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/AssetProfileExportData.java @@ -0,0 +1,28 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.asset.AssetProfile; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class AssetProfileExportData extends CalculatedFieldExportData { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java new file mode 100644 index 0000000000..7741f88822 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/CalculatedFieldExportData.java @@ -0,0 +1,56 @@ +/** + * 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.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.cf.CalculatedField; + +import java.util.Comparator; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CalculatedFieldExportData> extends EntityExportData { + + public static final Comparator calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName); + + @JsonProperty(index = 102) + @JsonIgnoreProperties({"entityId", "createdTime", "version"}) + private List calculatedFields; + + @JsonIgnore + @Override + public boolean hasCalculatedFields() { + return calculatedFields != null; + } + + @Override + public CalculatedFieldExportData sort() { + super.sort(); + if (calculatedFields != null && !calculatedFields.isEmpty()) { + calculatedFields.sort(calculatedFieldsComparator); + } + return this; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java index bfa82caa2b..a391067c74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceExportData.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Data -public class DeviceExportData extends EntityExportData { +public class DeviceExportData extends CalculatedFieldExportData { @JsonProperty(index = 3) @JsonIgnoreProperties({"id", "deviceId", "createdTime", "version"}) @@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData { public boolean hasCredentials() { return credentials != null; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java new file mode 100644 index 0000000000..b6b4d9bdcf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/DeviceProfileExportData.java @@ -0,0 +1,28 @@ +/** + * 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 lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.DeviceProfile; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Data +public class DeviceProfileExportData extends CalculatedFieldExportData { + +} 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 7bc9a63c29..5b7d95db1a 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 @@ -37,7 +37,10 @@ import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "entityType", include = As.EXISTING_PROPERTY, visible = true, defaultImpl = EntityExportData.class) @JsonSubTypes({ + @Type(name = "DEVICE_PROFILE", value = DeviceProfileExportData.class), + @Type(name = "ASSET_PROFILE", value = AssetProfileExportData.class), @Type(name = "DEVICE", value = DeviceExportData.class), + @Type(name = "ASSET", value = AssetExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) @@ -96,4 +99,9 @@ public class EntityExportData> { return relations != null; } + @JsonIgnore + public boolean hasCalculatedFields() { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java index 46dc6860ee..078ca29e28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportSettings.java @@ -25,7 +25,10 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityExportSettings { + private boolean exportRelations; private boolean exportAttributes; private boolean exportCredentials; + private boolean exportCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java index 2abdb55b65..d49801537c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityImportSettings.java @@ -25,8 +25,11 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @Builder public class EntityImportSettings { + private boolean findExistingByName; private boolean updateRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java index 5a02b18364..6d1ac2283b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/VersionCreateConfig.java @@ -17,13 +17,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import lombok.Data; +import java.io.Serial; import java.io.Serializable; @Data public class VersionCreateConfig implements Serializable { + + @Serial private static final long serialVersionUID = 1223723167716612772L; private boolean saveRelations; private boolean saveAttributes; private boolean saveCredentials; + private boolean saveCalculatedFields; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java index 0ae263dada..7f3ce89372 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/load/VersionLoadConfig.java @@ -23,5 +23,6 @@ public class VersionLoadConfig { private boolean loadRelations; private boolean loadAttributes; private boolean loadCredentials; + private boolean loadCalculatedFields; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 416bff6ea3..dceef73aba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.cf; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java index 2668a1a0d8..3a529b7d93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.Table; import lombok.Data; @@ -60,6 +58,15 @@ public class CalculatedFieldLinkEntity extends BaseSqlEntity