Merge pull request #12905 from dashevchenko/resource_vc_fix

Version control: fixed resource restoring
This commit is contained in:
Viacheslav Klimov 2025-05-07 13:18:43 +03:00 committed by GitHub
commit 51636229d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 129 additions and 43 deletions

View File

@ -46,7 +46,7 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
}
@Override
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) {
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider, CompareResult compareResult) {
Asset savedAsset = assetService.saveAsset(asset);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedAsset, exportData, idProvider);

View File

@ -49,7 +49,7 @@ public class AssetProfileImportService extends BaseEntityImportService<AssetProf
}
@Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);

View File

@ -18,6 +18,8 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.api.client.util.Objects;
import com.google.common.util.concurrent.FutureCallback;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -117,10 +119,10 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
E prepared = prepare(ctx, entity, existingEntity, exportData, idProvider);
boolean saveOrUpdate = existingEntity == null || compare(ctx, exportData, prepared, existingEntity);
CompareResult compareResult = compare(ctx, exportData, prepared, existingEntity);
if (saveOrUpdate) {
E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider);
if (compareResult.isUpdateNeeded()) {
E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider, compareResult);
boolean created = existingEntity == null;
importResult.setCreated(created);
importResult.setUpdated(!created);
@ -137,6 +139,17 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
return importResult;
}
@Data
@AllArgsConstructor
static class CompareResult {
private boolean updateNeeded;
private boolean externalIdChangedOnly;
public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded;
}
}
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
return importCalculatedFields(ctx, prepared, exportData, idProvider);
}
@ -148,18 +161,30 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
protected abstract E prepare(EntitiesImportCtx ctx, E entity, E oldEntity, D exportData, IdProvider idProvider);
protected boolean compare(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
protected CompareResult compare(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
if (existing == null) {
log.debug("[{}] Found new entity.", prepared.getId());
return new CompareResult(true);
}
var newCopy = deepCopy(prepared);
var existingCopy = deepCopy(existing);
cleanupForComparison(newCopy);
cleanupForComparison(existingCopy);
var result = !newCopy.equals(existingCopy);
if (result) {
var updateNeeded = isUpdateNeeded(ctx, exportData, newCopy, existingCopy);
boolean externalIdChangedOnly = false;
if (updateNeeded) {
log.debug("[{}] Found update.", prepared.getId());
log.debug("[{}] From: {}", prepared.getId(), newCopy);
log.debug("[{}] To: {}", prepared.getId(), existingCopy);
cleanupExternalId(newCopy);
cleanupExternalId(existingCopy);
externalIdChangedOnly = newCopy.equals(existingCopy);
}
return result;
return new CompareResult(updateNeeded, externalIdChangedOnly);
}
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, D exportData, E prepared, E existing) {
return !prepared.equals(existing);
}
protected abstract E deepCopy(E e);
@ -172,7 +197,11 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
}
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider);
protected void cleanupExternalId(E e) {
e.setExternalId(null);
}
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult);
protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {

View File

@ -52,7 +52,7 @@ public class CustomerImportService extends BaseEntityImportService<CustomerId, C
}
@Override
protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider) {
protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData<Customer> exportData, IdProvider idProvider, CompareResult compareResult) {
if (!customer.isPublic()) {
return customerService.saveCustomer(customer);
} else {

View File

@ -75,7 +75,7 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
}
@Override
protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData<Dashboard> exportData, IdProvider idProvider) {
protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData<Dashboard> exportData, IdProvider idProvider, CompareResult compareResult) {
var tenantId = ctx.getTenantId();
Set<ShortCustomerInfo> assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream()
@ -116,8 +116,8 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
}
@Override
protected boolean compare(EntitiesImportCtx ctx, EntityExportData<Dashboard> exportData, Dashboard prepared, Dashboard existing) {
return super.compare(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration());
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, EntityExportData<Dashboard> exportData, Dashboard prepared, Dashboard existing) {
return super.isUpdateNeeded(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration());
}
@Override

View File

@ -63,7 +63,7 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
}
@Override
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider, CompareResult compareResult) {
Device savedDevice;
if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
exportData.getCredentials().setId(null);

View File

@ -51,7 +51,7 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
}
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);

View File

@ -55,7 +55,7 @@ public class EntityViewImportService extends BaseEntityImportService<EntityViewI
}
@Override
protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData<EntityView> exportData, IdProvider idProvider) {
protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData<EntityView> exportData, IdProvider idProvider, CompareResult compareResult) {
return entityViewService.saveEntityView(entityView);
}

View File

@ -135,7 +135,7 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
}
@Override
protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData<NotificationRule> exportData, IdProvider idProvider) {
protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData<NotificationRule> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationRule);
return notificationRuleService.saveNotificationRule(ctx.getTenantId(), notificationRule);
}

View File

@ -80,7 +80,7 @@ public class NotificationTargetImportService extends BaseEntityImportService<Not
}
@Override
protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData<NotificationTarget> exportData, IdProvider idProvider) {
protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData<NotificationTarget> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationTarget);
return notificationTargetService.saveNotificationTarget(ctx.getTenantId(), notificationTarget);
}

View File

@ -48,7 +48,7 @@ public class NotificationTemplateImportService extends BaseEntityImportService<N
}
@Override
protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData<NotificationTemplate> exportData, IdProvider idProvider) {
protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData<NotificationTemplate> exportData, IdProvider idProvider, CompareResult compareResult) {
ConstraintValidator.validateFields(notificationTemplate);
return notificationTemplateService.saveNotificationTemplate(ctx.getTenantId(), notificationTemplate);
}

View File

@ -57,22 +57,30 @@ public class ResourceImportService extends BaseEntityImportService<TbResourceId,
return existingResource;
}
@Override
protected boolean compare(EntitiesImportCtx ctx, EntityExportData<TbResource> exportData, TbResource prepared, TbResource existing) {
return true;
}
@Override
protected TbResource deepCopy(TbResource resource) {
return new TbResource(resource);
}
@Override
protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData<TbResource> exportData, IdProvider idProvider) {
protected void cleanupForComparison(TbResource resource) {
super.cleanupForComparison(resource);
resource.setSearchText(null);
if (resource.getDescriptor().isNull()) {
resource.setDescriptor(null);
}
}
@Override
protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData<TbResource> exportData, IdProvider idProvider, CompareResult compareResult) {
if (resource.getResourceType() == ResourceType.IMAGE) {
return new TbResource(imageService.saveImage(resource));
} else {
if (compareResult.isExternalIdChangedOnly()) {
resource = resourceService.saveResource(resource, false);
} else {
resource = resourceService.saveResource(resource);
}
resource.setData(null);
resource.setPreview(null);
return resource;

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeDao;
@ -103,7 +105,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
}
@Override
protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider) {
protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider, CompareResult compareResult) {
ruleChain = ruleChainService.saveRuleChain(ruleChain);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
exportData.getMetaData().setRuleChainId(ruleChain.getId());
@ -115,15 +117,15 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
}
@Override
protected boolean compare(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
boolean different = super.compare(ctx, exportData, prepared, existing);
if (!different) {
protected boolean isUpdateNeeded(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
boolean updateNeeded = super.isUpdateNeeded(ctx, exportData, prepared, existing);
if (!updateNeeded) {
RuleChainMetaData newMD = exportData.getMetaData();
RuleChainMetaData existingMD = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), prepared.getId());
existingMD.setRuleChainId(null);
different = !newMD.equals(existingMD);
updateNeeded = !newMD.equals(existingMD);
}
return different;
return updateNeeded;
}
@Override

View File

@ -44,13 +44,13 @@ public class WidgetTypeImportService extends BaseEntityImportService<WidgetTypeI
}
@Override
protected WidgetTypeDetails saveOrUpdate(EntitiesImportCtx ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData, IdProvider idProvider) {
protected WidgetTypeDetails saveOrUpdate(EntitiesImportCtx ctx, WidgetTypeDetails widgetsBundle, WidgetTypeExportData exportData, IdProvider idProvider, CompareResult compareResult) {
return widgetTypeService.saveWidgetType(widgetsBundle);
}
@Override
protected boolean compare(EntitiesImportCtx ctx, WidgetTypeExportData exportData, WidgetTypeDetails prepared, WidgetTypeDetails existing) {
return true;
protected CompareResult compare(EntitiesImportCtx ctx, WidgetTypeExportData exportData, WidgetTypeDetails prepared, WidgetTypeDetails existing) {
return new CompareResult(true);
}
@Override

View File

@ -49,7 +49,7 @@ public class WidgetsBundleImportService extends BaseEntityImportService<WidgetsB
}
@Override
protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider) {
protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (CollectionsUtil.isNotEmpty(exportData.getWidgets())) {
exportData.getWidgets().forEach(widgetTypeNode -> {
String bundleAlias = widgetTypeNode.remove("bundleAlias").asText();
@ -75,8 +75,8 @@ public class WidgetsBundleImportService extends BaseEntityImportService<WidgetsB
}
@Override
protected boolean compare(EntitiesImportCtx ctx, WidgetsBundleExportData exportData, WidgetsBundle prepared, WidgetsBundle existing) {
return true;
protected CompareResult compare(EntitiesImportCtx ctx, WidgetsBundleExportData exportData, WidgetsBundle prepared, WidgetsBundle existing) {
return new CompareResult(true);
}
@Override

View File

@ -454,7 +454,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
private VersionLoadResult onError(EntityId externalId, Throwable e) {
return analyze(e, externalId).orElse(VersionLoadResult.error(EntityLoadError.runtimeError(e)));
return analyze(e, externalId).orElse(VersionLoadResult.error(EntityLoadError.runtimeError(e, externalId)));
}
private Optional<VersionLoadResult> analyze(Throwable e, EntityId externalId) {

View File

@ -40,6 +40,9 @@ import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
@ -113,6 +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;
@DaoSqlTest
public class VersionControlTest extends AbstractControllerTest {
@ -648,6 +653,26 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
@Test
public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource");
String versionId = createVersion("resources", EntityType.TB_RESOURCE);
TbResource resource = findResource(resourceInfo.getName());
loadVersion(versionId, EntityType.TB_RESOURCE);
TbResource importedResource = findResource(resource.getName());
checkImportedEntity(tenantId1, resource, tenantId1, importedResource);
checkImportedResourceData(resource, importedResource);
}
protected void checkImportedResourceData(TbResource resource, TbResource importedResource) {
assertThat(importedResource.getName()).isEqualTo(resource.getName());
assertThat(importedResource.getData()).isEqualTo(resource.getData());
assertThat(importedResource.getResourceKey()).isEqualTo(resource.getResourceKey());
assertThat(importedResource.getResourceType()).isEqualTo(resource.getResourceType());
}
private <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) {
assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1);
assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2);
@ -1128,4 +1153,22 @@ public class VersionControlTest extends AbstractControllerTest {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData();
}
private TbResourceInfo createResource(String name) {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle(name);
resource.setFileName(JS_TEST_FILE_NAME);
resource.setEncodedData(TEST_DATA);
return saveTbResource(resource);
}
private TbResourceInfo saveTbResource(TbResource tbResource) {
return doPost("/api/resource", tbResource, TbResourceInfo.class);
}
private TbResource findResource(String name) throws Exception {
return doGetTypedWithPageLink("/api/resource?", new TypeReference<PageData<TbResource>>() {}, new PageLink(100, 0, name)).getData().get(0);
}
}

View File

@ -45,11 +45,15 @@ public class EntityLoadError implements Serializable {
}
public static EntityLoadError runtimeError(Throwable e) {
return runtimeError(e, null);
}
public static EntityLoadError runtimeError(Throwable e, EntityId externalId) {
String message = e.getMessage();
if (StringUtils.isEmpty(message)) {
message = "unexpected error (" + ClassUtils.getShortClassName(e.getClass()) + ")";
}
return EntityLoadError.builder().type("RUNTIME").message(message).build();
return EntityLoadError.builder().type("RUNTIME").message(message).source(externalId).build();
}
}