diff --git a/application/pom.xml b/application/pom.xml
index 2d6dda0bbe..6d1e009d00 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -69,6 +69,10 @@
org.thingsboard.common
cluster-api
+
+ org.thingsboard.common
+ version-control
+
org.thingsboard.rule-engine
rule-engine-components
diff --git a/application/src/main/data/upgrade/3.3.4/schema_update.sql b/application/src/main/data/upgrade/3.3.4/schema_update.sql
index 7584d2f732..69df2afdc9 100644
--- a/application/src/main/data/upgrade/3.3.4/schema_update.sql
+++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql
@@ -14,6 +14,40 @@
-- limitations under the License.
--
+ALTER TABLE device
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE device_profile
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE asset
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE rule_chain
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE rule_node
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE dashboard
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE customer
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE widgets_bundle
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE entity_view
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+
+CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_device_profile_external_id ON device_profile(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_asset_external_id ON asset(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_rule_chain_external_id ON rule_chain(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_dashboard_external_id ON dashboard(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_customer_external_id ON customer(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tenant_id, external_id);
+CREATE INDEX IF NOT EXISTS idx_entity_view_external_id ON entity_view(tenant_id, external_id);
+
+CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type);
+
+ALTER TABLE admin_settings
+ ADD COLUMN IF NOT EXISTS tenant_id uuid NOT NULL DEFAULT '13814000-1dd2-11b2-8080-808080808080';
+
CREATE TABLE IF NOT EXISTS queue (
id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY,
created_time bigint NOT NULL,
@@ -35,3 +69,4 @@ CREATE TABLE IF NOT EXISTS user_auth_settings (
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
two_fa_settings varchar
);
+
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 500ad60524..733cf75e64 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -48,7 +48,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
index 1415ff4ece..86675a79a8 100644
--- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
@@ -15,9 +15,11 @@
*/
package org.thingsboard.server.config;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
+import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@@ -41,6 +43,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+@Slf4j
@Component
public class RateLimitProcessingFilter extends GenericFilterBean {
@@ -58,7 +61,13 @@ public class RateLimitProcessingFilter extends GenericFilterBean {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
SecurityUser user = getCurrentUser();
if (user != null && !user.isSystemAdmin()) {
- var profileConfiguration = tenantProfileCache.get(user.getTenantId()).getDefaultProfileConfiguration();
+ var profile = tenantProfileCache.get(user.getTenantId());
+ if (profile == null) {
+ log.debug("[{}] Failed to lookup tenant profile", user.getTenantId());
+ errorResponseHandler.handle(new BadCredentialsException("Failed to lookup tenant profile"), (HttpServletResponse) response);
+ return;
+ }
+ var profileConfiguration = profile.getDefaultProfileConfiguration();
if (!checkRateLimits(user.getTenantId(), profileConfiguration.getTenantServerRestLimitsConfiguration(), perTenantLimits, response)) {
return;
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e5934cbf26..54cbdd9b0f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -16,16 +16,16 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.PathVariable;
-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.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
@@ -34,14 +34,18 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
+import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
+import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.update.UpdateService;
-import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.*;
@RestController
@TbCoreComponent
@@ -60,6 +64,12 @@ public class AdminController extends BaseController {
@Autowired
private SystemSecurityService systemSecurityService;
+ @Autowired
+ private EntitiesVersionControlService versionControlService;
+
+ @Autowired
+ private TbAutoCommitSettingsService autoCommitSettingsService;
+
@Autowired
private UpdateService updateService;
@@ -96,6 +106,7 @@ public class AdminController extends BaseController {
@RequestBody AdminSettings adminSettings) throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
+ adminSettings.setTenantId(getTenantId());
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) {
mailService.updateMailConfiguration();
@@ -180,6 +191,137 @@ public class AdminController extends BaseController {
}
}
+ @ApiOperation(value = "Get repository settings (getRepositorySettings)",
+ notes = "Get the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/repositorySettings")
+ @ResponseBody
+ public RepositorySettings getRepositorySettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ RepositorySettings versionControlSettings = checkNotNull(versionControlService.getVersionControlSettings(getTenantId()));
+ versionControlSettings.setPassword(null);
+ versionControlSettings.setPrivateKey(null);
+ versionControlSettings.setPrivateKeyPassword(null);
+ return versionControlSettings;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Check repository settings exists (repositorySettingsExists)",
+ notes = "Check whether the repository settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/repositorySettings/exists")
+ @ResponseBody
+ public Boolean repositorySettingsExists() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return versionControlService.getVersionControlSettings(getTenantId()) != null;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Creates or Updates the repository settings (saveRepositorySettings)",
+ notes = "Creates or Updates the repository settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @PostMapping("/repositorySettings")
+ public DeferredResult saveRepositorySettings(@RequestBody RepositorySettings settings) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ ListenableFuture future = versionControlService.saveVersionControlSettings(getTenantId(), settings);
+ return wrapFuture(Futures.transform(future, savedSettings -> {
+ savedSettings.setPassword(null);
+ savedSettings.setPrivateKey(null);
+ savedSettings.setPrivateKeyPassword(null);
+ return savedSettings;
+ }, MoreExecutors.directExecutor()));
+ }
+
+ @ApiOperation(value = "Delete repository settings (deleteRepositorySettings)",
+ notes = "Deletes the repository settings."
+ + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public DeferredResult deleteRepositorySettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
+ return wrapFuture(versionControlService.deleteVersionControlSettings(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @ApiOperation(value = "Check repository access (checkRepositoryAccess)",
+ notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST)
+ public DeferredResult checkRepositoryAccess(
+ @ApiParam(value = "A JSON value representing the Repository Settings.")
+ @RequestBody RepositorySettings settings) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ settings = checkNotNull(settings);
+ return wrapFuture(versionControlService.checkVersionControlAccess(getTenantId(), settings));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Get auto commit settings (getAutoCommitSettings)",
+ notes = "Get the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/autoCommitSettings")
+ @ResponseBody
+ public AutoCommitSettings getAutoCommitSettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return checkNotNull(autoCommitSettingsService.get(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Check auto commit settings exists (autoCommitSettingsExists)",
+ notes = "Check whether the auto commit settings exists. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @GetMapping("/autoCommitSettings/exists")
+ @ResponseBody
+ public Boolean autoCommitSettingsExists() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return autoCommitSettingsService.get(getTenantId()) != null;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "Creates or Updates the auto commit settings (saveAutoCommitSettings)",
+ notes = "Creates or Updates the auto commit settings object. " + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @PostMapping("/autoCommitSettings")
+ public AutoCommitSettings saveAutoCommitSettings(@RequestBody AutoCommitSettings settings) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ return autoCommitSettingsService.save(getTenantId(), settings);
+ }
+
+ @ApiOperation(value = "Delete auto commit settings (deleteAutoCommitSettings)",
+ notes = "Deletes the auto commit settings."
+ + TENANT_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteAutoCommitSettings() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
+ autoCommitSettingsService.delete(getTenantId());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
@ApiOperation(value = "Check for new Platform Releases (checkUpdates)",
notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index b0ed1ed01d..939e9da8a0 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -226,11 +226,7 @@ public class AlarmController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
try {
- if (getCurrentUser().isCustomerUser()) {
- return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
- } else {
- return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
- }
+ return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get());
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 4718feb774..2a3268146f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -51,9 +51,9 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 050745844b..587354c0c4 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -18,6 +18,10 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -29,6 +33,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@@ -138,6 +143,7 @@ import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@@ -281,6 +287,9 @@ public abstract class BaseController {
@Autowired
protected QueueService queueService;
+ @Autowired
+ protected EntitiesVersionControlService vcService;
+
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@@ -989,4 +998,20 @@ public abstract class BaseController {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
+
+ protected DeferredResult wrapFuture(ListenableFuture future) {
+ final DeferredResult deferredResult = new DeferredResult<>();
+ Futures.addCallback(future, new FutureCallback<>() {
+ @Override
+ public void onSuccess(T result) {
+ deferredResult.setResult(result);
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ deferredResult.setErrorResult(t);
+ }
+ }, MoreExecutors.directExecutor());
+ return deferredResult;
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
index a26493000a..63be9a437a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -135,6 +135,8 @@ public class ControllerConstants {
protected static final String EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Assignment works in async way - first, notification event pushed to edge service queue on platform. ";
protected static final String EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform). ";
+ protected static final String ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the entity version name.";
+
protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n";
protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 47acdc64a4..d0f40a476b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -631,7 +631,7 @@ public class DashboardController extends BaseController {
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId, Operation.READ);
- return tbDashboardService.asignDashboardToEdge(dashboardId, edge, getCurrentUser());
+ return tbDashboardService.assignDashboardToEdge(dashboardId, edge, getCurrentUser());
}
@ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 793ea64336..2881787d95 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -66,9 +66,9 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.device.DeviceBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
index 5d9737db23..4dcffa605e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
@@ -52,8 +52,8 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeBulkImportService;
import org.thingsboard.server.service.entitiy.edge.TbEdgeService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
new file mode 100644
index 0000000000..701719af58
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
@@ -0,0 +1,368 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
+import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
+import org.thingsboard.server.common.data.sync.vc.EntityVersion;
+import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
+import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
+import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
+import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api/entities/vc")
+@PreAuthorize("hasAuthority('TENANT_ADMIN')")
+@RequiredArgsConstructor
+public class EntitiesVersionControlController extends BaseController {
+
+ private final EntitiesVersionControlService versionControlService;
+
+
+ @ApiOperation(value = "", notes = "" +
+ "SINGLE_ENTITY:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"SINGLE_ENTITY\",\n" +
+ "\n" +
+ " \"versionName\": \"Version 1.0\",\n" +
+ " \"branch\": \"dev\",\n" +
+ "\n" +
+ " \"entityId\": {\n" +
+ " \"entityType\": \"DEVICE\",\n" +
+ " \"id\": \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " },\n" +
+ " \"config\": {\n" +
+ " \"saveRelations\": true\n" +
+ " }\n" +
+ "}\n```" + NEW_LINE +
+ "COMPLEX:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"COMPLEX\",\n" +
+ "\n" +
+ " \"versionName\": \"Devices and profiles: release 2\",\n" +
+ " \"branch\": \"master\",\n" +
+ "\n" +
+ " \"syncStrategy\": \"OVERWRITE\",\n" +
+ " \"entityTypes\": {\n" +
+ " \"DEVICE\": {\n" +
+ " \"syncStrategy\": null,\n" +
+ " \"allEntities\": true,\n" +
+ " \"saveRelations\": true\n" +
+ " },\n" +
+ " \"DEVICE_PROFILE\": {\n" +
+ " \"syncStrategy\": \"MERGE\",\n" +
+ " \"allEntities\": false,\n" +
+ " \"entityIds\": [\n" +
+ " \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " ],\n" +
+ " \"saveRelations\": true\n" +
+ " }\n" +
+ " }\n" +
+ "}\n```")
+ @PostMapping("/version")
+ public DeferredResult saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
+ return wrapFuture(versionControlService.saveEntitiesVersion(user, request));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Device profile 1 version 1.0\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}/{entityType}/{externalEntityUuid}", params = {"pageSize", "page"})
+ public DeferredResult> listEntityVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID externalEntityUuid,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Device profiles from dev\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}/{entityType}", params = {"pageSize", "page"})
+ public DeferredResult> listEntityTypeVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"ba9baaca1742b730e7331f31a6a51da5fc7da7f7\",\n" +
+ " \"name\": \"Device 1 removed\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ " \"name\": \"Device profiles added\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Devices added\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping(value = "/version/{branch}", params = {"pageSize", "page"})
+ public DeferredResult> listVersions(@PathVariable String branch,
+ @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @ApiParam(value = ENTITY_VERSION_TEXT_SEARCH_DESCRIPTION)
+ @RequestParam(required = false) String textSearch,
+ @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
+ @RequestParam(required = false) String sortProperty,
+ @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @GetMapping("/entity/{branch}/{entityType}/{versionId}")
+ public DeferredResult> listEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/entity/{branch}/{versionId}")
+ public DeferredResult> listAllEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}")
+ public DeferredResult getEntityDataInfo(@PathVariable String versionId,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID externalEntityUuid) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
+ return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}")
+ public DeferredResult compareEntityDataToVersion(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID internalEntityUuid,
+ @RequestParam String versionId) throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid);
+ return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "SINGLE_ENTITY:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"SINGLE_ENTITY\",\n" +
+ " \n" +
+ " \"branch\": \"dev\",\n" +
+ " \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ " \n" +
+ " \"externalEntityId\": {\n" +
+ " \"entityType\": \"DEVICE\",\n" +
+ " \"id\": \"b7944123-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " },\n" +
+ " \"config\": {\n" +
+ " \"loadRelations\": false,\n" +
+ " \"findExistingEntityByName\": false\n" +
+ " }\n" +
+ "}\n```" + NEW_LINE +
+ "ENTITY_TYPE:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"ENTITY_TYPE\",\n" +
+ "\n" +
+ " \"branch\": \"dev\",\n" +
+ " \"versionId\": \"b3c28d722d328324c7c15b0b30047b0c40011cf7\",\n" +
+ "\n" +
+ " \"entityTypes\": {\n" +
+ " \"DEVICE\": {\n" +
+ " \"loadRelations\": false,\n" +
+ " \"findExistingEntityByName\": false,\n" +
+ " \"removeOtherEntities\": true\n" +
+ " }\n" +
+ " }\n" +
+ "}\n```")
+ @PostMapping("/entity")
+ public DeferredResult loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.READ);
+ return wrapFuture(versionControlService.loadEntitiesVersion(user, request));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"name\": \"master\",\n" +
+ " \"default\": true\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"dev\",\n" +
+ " \"default\": false\n" +
+ " },\n" +
+ " {\n" +
+ " \"name\": \"dev-2\",\n" +
+ " \"default\": false\n" +
+ " }\n" +
+ "]\n\n```")
+ @GetMapping("/branches")
+ public DeferredResult> listBranches() throws ThingsboardException {
+ try {
+ accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
+ final TenantId tenantId = getTenantId();
+ ListenableFuture> branches = versionControlService.listBranches(tenantId);
+ return wrapFuture(Futures.transform(branches, remoteBranches -> {
+ List infos = new ArrayList<>();
+
+ String defaultBranch = versionControlService.getVersionControlSettings(tenantId).getDefaultBranch();
+ if (StringUtils.isNotEmpty(defaultBranch)) {
+ infos.add(new BranchInfo(defaultBranch, true));
+ }
+
+ remoteBranches.forEach(branch -> {
+ if (!branch.equals(defaultBranch)) {
+ infos.add(new BranchInfo(branch, false));
+ }
+ });
+ return infos;
+ }, MoreExecutors.directExecutor()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @Data
+ public static class BranchInfo {
+ private final String name;
+ private final boolean isDefault;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 190d68e79e..a588004b79 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -233,7 +233,6 @@ public class RuleChainController extends BaseController {
public RuleChain saveRuleChain(
@ApiParam(value = "A JSON value representing the rule chain.")
@RequestBody RuleChain ruleChain) throws ThingsboardException {
-
ruleChain.setTenantId(getCurrentUser().getTenantId());
checkEntity(ruleChain.getId(), ruleChain, Resource.RULE_CHAIN);
return tbRuleChainService.save(ruleChain, getCurrentUser());
diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
index b8c0656bd2..2a64db7385 100644
--- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
@@ -22,13 +22,14 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
+import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -41,7 +42,6 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.cluster.TbClusterService;
import java.util.List;
import java.util.Map;
@@ -212,7 +212,7 @@ public class EntityActionService {
}
}
- public void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
+ public void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();
@@ -223,6 +223,9 @@ public class EntityActionService {
auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
}
+ public void sendEntityNotificationMsgToEdgeService(TenantId tenantId, EntityId entityId, EdgeEventActionType action) {
+ tbClusterService.sendNotificationMsgToEdgeService(tenantId, null, entityId, null, null, action);
+ }
private T extractParameter(Class clazz, int index, Object... additionalInfo) {
T result = null;
@@ -267,4 +270,5 @@ public class EntityActionService {
entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
}
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
new file mode 100644
index 0000000000..55b23a99cc
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2022 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.apiusage;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
+import org.thingsboard.server.common.msg.tools.TbRateLimits;
+import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+@Service
+@RequiredArgsConstructor
+public class DefaultRateLimitService implements RateLimitService {
+
+ private final TbTenantProfileCache tenantProfileCache;
+
+ private final Map> rateLimits = new ConcurrentHashMap<>();
+
+ @Override
+ public boolean checkEntityExportLimit(TenantId tenantId) {
+ return checkLimit(tenantId, "entityExport", DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit);
+ }
+
+ @Override
+ public boolean checkEntityImportLimit(TenantId tenantId) {
+ return checkLimit(tenantId, "entityImport", DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit);
+ }
+
+ private boolean checkLimit(TenantId tenantId, String rateLimitsKey, Function rateLimitConfigExtractor) {
+ String rateLimitConfig = tenantProfileCache.get(tenantId).getProfileConfiguration()
+ .map(rateLimitConfigExtractor).orElse(null);
+
+ Map rateLimits = this.rateLimits.get(rateLimitsKey);
+ if (StringUtils.isEmpty(rateLimitConfig)) {
+ if (rateLimits != null) {
+ rateLimits.remove(tenantId);
+ if (rateLimits.isEmpty()) {
+ this.rateLimits.remove(rateLimitsKey);
+ }
+ }
+ return true;
+ }
+
+ if (rateLimits == null) {
+ rateLimits = new ConcurrentHashMap<>();
+ this.rateLimits.put(rateLimitsKey, rateLimits);
+ }
+ TbRateLimits rateLimit = rateLimits.get(tenantId);
+ if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
+ rateLimit = new TbRateLimits(rateLimitConfig);
+ rateLimits.put(tenantId, rateLimit);
+ }
+
+ return rateLimit.tryConsume();
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
new file mode 100644
index 0000000000..d3d4244ca1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/RateLimitService.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2022 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.apiusage;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public interface RateLimitService {
+
+ boolean checkEntityExportLimit(TenantId tenantId);
+
+ boolean checkEntityImportLimit(TenantId tenantId);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
index e9999d2dc1..5eaeec0b73 100644
--- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
@@ -26,9 +26,9 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
index 34c30d5225..f0a7b6231e 100644
--- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
@@ -49,9 +49,9 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.device.TbDeviceService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Collection;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
index fd55e9b321..404e5af5fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java
@@ -28,9 +28,9 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.entitiy.edge.TbEdgeService;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
index 77dc121e38..7943ec0595 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceProfileMsgConstructor.java
@@ -20,7 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceProfileId;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
index 0f4ecf7e30..ee10c7859b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java
@@ -83,7 +83,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailSettings)));
- AdminSettings tenantMailSettings = convertToTenantAdminSettings(systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
+ AdminSettings tenantMailSettings = convertToTenantAdminSettings(tenantId, systemMailSettings.getKey(), (ObjectNode) systemMailSettings.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailSettings)));
@@ -91,7 +91,7 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(systemMailTemplates)));
- AdminSettings tenantMailTemplates = convertToTenantAdminSettings(systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
+ AdminSettings tenantMailTemplates = convertToTenantAdminSettings(tenantId, systemMailTemplates.getKey(), (ObjectNode) systemMailTemplates.getJsonValue());
result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ADMIN_SETTINGS,
EdgeEventActionType.UPDATED, null, mapper.valueToTree(tenantMailTemplates)));
@@ -151,8 +151,9 @@ public class AdminSettingsEdgeEventFetcher implements EdgeEventFetcher {
}
}
- private AdminSettings convertToTenantAdminSettings(String key, ObjectNode jsonValue) {
+ private AdminSettings convertToTenantAdminSettings(TenantId tenantId, String key, ObjectNode jsonValue) {
AdminSettings tenantMailSettings = new AdminSettings();
+ tenantMailSettings.setTenantId(tenantId);
jsonValue.put("useSystemMailSettings", true);
tenantMailSettings.setJsonValue(jsonValue);
tenantMailSettings.setKey(key);
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
index 9d87cabb2c..dbbb6ddb1f 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
@@ -378,7 +378,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
List> futures = new ArrayList<>();
for (EntityView entityView : entityViews) {
- ListenableFuture future = relationService.checkRelation(tenantId, edge.getId(), entityView.getId(),
+ ListenableFuture future = relationService.checkRelationAsync(tenantId, edge.getId(), entityView.getId(),
EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE);
futures.add(Futures.transformAsync(future, result -> {
if (Boolean.TRUE.equals(result)) {
@@ -413,11 +413,11 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
private ListenableFuture saveEdgeEvent(TenantId tenantId,
- EdgeId edgeId,
- EdgeEventType type,
- EdgeEventActionType action,
- EntityId entityId,
- JsonNode body) {
+ EdgeId edgeId,
+ EdgeEventType type,
+ EdgeEventActionType action,
+ EntityId entityId,
+ JsonNode body) {
log.trace("Pushing edge event to edge queue. tenantId [{}], edgeId [{}], type [{}], action[{}], entityId [{}], body [{}]",
tenantId, edgeId, type, action, entityId, body);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
index 395ebdeaac..8439c1780b 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
@@ -64,6 +64,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.resource.TbResourceService;
@@ -125,6 +126,8 @@ public abstract class AbstractTbEntityService {
@Autowired
protected DashboardService dashboardService;
@Autowired
+ protected EntitiesVersionControlService vcService;
+ @Autowired
protected EntityViewService entityViewService;
@Autowired
protected TelemetrySubscriptionService tsSubService;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
index 492464f147..4ee8554c4a 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
@@ -46,6 +46,7 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
TenantId tenantId = asset.getTenantId();
try {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
+ vcService.autoCommit(user, savedAsset.getId());
notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedAsset.getId(), asset, savedAsset.getCustomerId(), actionType, user);
return savedAsset;
} catch (Exception e) {
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
index d35cb06a85..536e1be976 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
@@ -40,10 +40,10 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
public Customer save(Customer customer, SecurityUser user) throws ThingsboardException {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId();
- CustomerId customerId = customer.getId();
try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
- notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, customerId, actionType, user);
+ vcService.autoCommit(user, savedCustomer.getId());
+ notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), customer, null, actionType, user, e);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
index f7110c4da7..a475ff77da 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
@@ -48,6 +48,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
TenantId tenantId = dashboard.getTenantId();
try {
Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
+ vcService.autoCommit(user, savedDashboard.getId());
notificationEntityService.notifyCreateOrUpdateEntity(tenantId, savedDashboard.getId(), savedDashboard,
null, actionType, user);
return savedDashboard;
@@ -219,7 +220,7 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
}
@Override
- public Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException {
+ public Dashboard assignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_EDGE;
TenantId tenantId = user.getTenantId();
EdgeId edgeId = edge.getId();
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
index 84dba247ff..313fd737f9 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/TbDashboardService.java
@@ -26,7 +26,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set;
-public interface TbDashboardService extends SimpleTbEntityService {
+public interface TbDashboardService extends SimpleTbEntityService {
Dashboard assignDashboardToCustomer(DashboardId dashboardId, Customer customer, SecurityUser user) throws ThingsboardException;
@@ -40,7 +40,7 @@ public interface TbDashboardService extends SimpleTbEntityService {
Dashboard removeDashboardCustomers(Dashboard dashboard, Set customerIds, SecurityUser user) throws ThingsboardException;
- Dashboard asignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException;
+ Dashboard assignDashboardToEdge(DashboardId dashboardId, Edge edge, SecurityUser user) throws ThingsboardException;
Dashboard unassignDashboardFromEdge(Dashboard dashboard, Edge edge, SecurityUser user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
index d74610a462..5199887940 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
@@ -54,6 +54,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
+ vcService.autoCommit(user, savedDevice.getId());
notificationEntityService.notifyCreateOrUpdateDevice(tenantId, savedDevice.getId(), savedDevice.getCustomerId(),
savedDevice, oldDevice, actionType, user);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
index ca73b0cb83..3a0c1f1a23 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
@@ -54,7 +54,7 @@ public class DefaultTbDeviceProfileService extends AbstractTbEntityService imple
}
}
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
-
+ vcService.autoCommit(user, savedDeviceProfile.getId());
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedDeviceProfile.getId(),
actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
index 878c6d32bb..2eee167e75 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
@@ -66,19 +66,33 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
+ try {
+ EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
+ this.updateEntityViewAttributes(user, savedEntityView, existingEntityView);
+ notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
+ null, actionType, user);
+ return savedEntityView;
+ } catch (Exception e) {
+ notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.ENTITY_VIEW), entityView, null, actionType, user, e);
+ throw handleException(e);
+ }
+ }
+
+ @Override
+ public void updateEntityViewAttributes(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException {
try {
List> futures = new ArrayList<>();
- if (existingEntityView != null) {
- if (existingEntityView.getKeys() != null && existingEntityView.getKeys().getAttributes() != null) {
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.CLIENT_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SERVER_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
- futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SHARED_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
+
+ if (oldEntityView != null) {
+ if (oldEntityView.getKeys() != null && oldEntityView.getKeys().getAttributes() != null) {
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.CLIENT_SCOPE, oldEntityView.getKeys().getAttributes().getCs(), user));
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.SERVER_SCOPE, oldEntityView.getKeys().getAttributes().getSs(), user));
+ futures.add(deleteAttributesFromEntityView(oldEntityView, DataConstants.SHARED_SCOPE, oldEntityView.getKeys().getAttributes().getSh(), user));
}
- List tsKeys = existingEntityView.getKeys() != null && existingEntityView.getKeys().getTimeseries() != null ?
- existingEntityView.getKeys().getTimeseries() : Collections.emptyList();
- futures.add(deleteLatestFromEntityView(existingEntityView, tsKeys, user));
+ List tsKeys = oldEntityView.getKeys() != null && oldEntityView.getKeys().getTimeseries() != null ?
+ oldEntityView.getKeys().getTimeseries() : Collections.emptyList();
+ futures.add(deleteLatestFromEntityView(oldEntityView, tsKeys, user));
}
- EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
if (savedEntityView.getKeys() != null) {
if (savedEntityView.getKeys().getAttributes() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), user));
@@ -94,13 +108,7 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
-
- notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
- null, actionType, user);
-
- return savedEntityView;
} catch (Exception e) {
- notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.ENTITY_VIEW), entityView, null, actionType, user, e);
throw handleException(e);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
index dd5db9391b..e7e150531b 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
@@ -28,6 +28,8 @@ public interface TbEntityViewService {
EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException;
+ void updateEntityViewAttributes(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException;
+
void delete (EntityView entity, SecurityUser user) throws ThingsboardException;
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer,
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
index 2a8fc1bf0a..22baed8366 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
@@ -27,8 +27,10 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.queue.TbQueueService;
import org.thingsboard.server.service.install.InstallScripts;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.Collections;
+import java.util.concurrent.TimeUnit;
@Service
@TbCoreComponent
@@ -38,6 +40,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
private final InstallScripts installScripts;
private final TbQueueService tbQueueService;
private final TenantProfileService tenantProfileService;
+ private final EntitiesVersionControlService versionControlService;
@Override
public Tenant save(Tenant tenant) throws ThingsboardException {
@@ -70,6 +73,7 @@ public class DefaultTbTenantService extends AbstractTbEntityService implements T
tenantService.deleteTenant(tenantId);
tenantProfileCache.evict(tenantId);
notificationEntityService.notifyDeleteTenant(tenant);
+ versionControlService.deleteVersionControlSettings(tenantId).get(1, TimeUnit.MINUTES);
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index cece3377b0..50fcb28387 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -265,6 +265,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Override
public void createAdminSettings() throws Exception {
AdminSettings generalSettings = new AdminSettings();
+ generalSettings.setTenantId(TenantId.SYS_TENANT_ID);
generalSettings.setKey("general");
ObjectNode node = objectMapper.createObjectNode();
node.put("baseUrl", "http://localhost:8080");
@@ -273,6 +274,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
AdminSettings mailSettings = new AdminSettings();
+ mailSettings.setTenantId(TenantId.SYS_TENANT_ID);
mailSettings.setKey("mail");
node = objectMapper.createObjectNode();
node.put("mailFrom", "ThingsBoard ");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java
index 054f68f71d..c136f97b52 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java
@@ -1,5 +1,5 @@
/**
- * Copyright © 2016-2021 The Thingsboard Authors
+ * Copyright © 2016-2022 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.
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
index 2806c35cda..6b475f5a5a 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
@@ -53,7 +53,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
@@ -135,6 +135,15 @@ public class DefaultTbClusterService implements TbClusterService {
toCoreMsgs.incrementAndGet();
}
+ @Override
+ public void pushMsgToVersionControl(TenantId tenantId, TransportProtos.ToVersionControlServiceMsg msg, TbQueueCallback callback) {
+ TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_VC_EXECUTOR, tenantId, tenantId);
+ log.trace("PUSHING msg: {} to:{}", msg, tpi);
+ producerProvider.getTbVersionControlMsgProducer().send(tpi, new TbProtoQueueMsg<>(tenantId.getId(), msg), callback);
+ //TODO: ashvayka
+ toCoreMsgs.incrementAndGet();
+ }
+
@Override
public void pushNotificationToCore(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId);
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index 2ca47d689f..d071d591a1 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -36,7 +36,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory;
-import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@@ -75,6 +75,8 @@ import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import javax.annotation.PostConstruct;
@@ -117,6 +119,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> usageStatsConsumer;
private final TbQueueConsumer> firmwareStatesConsumer;
@@ -138,7 +141,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) {
RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
@@ -169,6 +166,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
ActionType actionType = ruleChain.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
+ vcService.autoCommit(user, savedRuleChain.getId());
if (RuleChainType.CORE.equals(savedRuleChain.getType())) {
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedRuleChain.getId(),
@@ -222,6 +220,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
public RuleChain saveDefaultByName(TenantId tenantId, DefaultRuleChainCreateRequest request, SecurityUser user) throws ThingsboardException {
try {
RuleChain savedRuleChain = installScripts.createDefaultRuleChain(tenantId, request.getName());
+ vcService.autoCommit(user, savedRuleChain.getId());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedRuleChain.getId(), ComponentLifecycleEvent.CREATED);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedRuleChain.getId(),
savedRuleChain, user, ActionType.ADDED, false, null);
@@ -281,6 +280,15 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
updatedRuleChains = Collections.emptyList();
}
+ if (updatedRuleChains.isEmpty()) {
+ vcService.autoCommit(user, ruleChainMetaData.getRuleChainId());
+ } else {
+ List uuids = new ArrayList<>(updatedRuleChains.size() + 1);
+ uuids.add(ruleChainMetaData.getRuleChainId().getId());
+ updatedRuleChains.forEach(rc -> uuids.add(rc.getId().getId()));
+ vcService.autoCommit(user, EntityType.RULE_CHAIN, uuids);
+ }
+
RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaDataId));
if (RuleChainType.CORE.equals(ruleChain.getType())) {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
index 63d3233928..419a60b3b9 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
@@ -63,7 +63,7 @@ public class SmsTwoFaProvider extends OtpBasedTwoFaProvider {
public SessionRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
- super(CacheConstants.ASSET_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() {
+ super(CacheConstants.SESSIONS_CACHE, cacheSpecsMap, connectionFactory, configuration, new RedisSerializer<>() {
@Override
public byte[] serialize(TransportProtos.DeviceSessionsCacheEntry deviceSessionsCacheEntry) throws SerializationException {
return deviceSessionsCacheEntry.toByteArray();
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
new file mode 100644
index 0000000000..b2abe2a581
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.sync.ThrowingRunnable;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.action.EntityActionService;
+import org.thingsboard.server.service.apiusage.RateLimitService;
+import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.impl.BaseEntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.impl.DefaultEntityExportService;
+import org.thingsboard.server.service.sync.ie.importing.EntityImportService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultEntitiesExportImportService implements EntitiesExportImportService {
+
+ private final Map> exportServices = new HashMap<>();
+ private final Map> importServices = new HashMap<>();
+
+ private final EntityActionService entityActionService;
+ private final RelationService relationService;
+ private final RateLimitService rateLimitService;
+
+ protected static final List SUPPORTED_ENTITY_TYPES = List.of(
+ EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN,
+ EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE,
+ EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE
+ );
+
+
+ @Override
+ public , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException {
+ if (!rateLimitService.checkEntityExportLimit(ctx.getTenantId())) {
+ throw new ThingsboardException("Rate limit for entities export is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
+ }
+
+ EntityType entityType = entityId.getEntityType();
+ EntityExportService> exportService = getExportService(entityType);
+
+ return exportService.getExportData(ctx, entityId);
+ }
+
+ @Override
+ public , I extends EntityId> EntityImportResult importEntity(EntitiesImportCtx ctx, EntityExportData exportData) throws ThingsboardException {
+ if (!rateLimitService.checkEntityImportLimit(ctx.getTenantId())) {
+ throw new ThingsboardException("Rate limit for entities import is exceeded", ThingsboardErrorCode.TOO_MANY_REQUESTS);
+ }
+ if (exportData.getEntity() == null || exportData.getEntity().getId() == null) {
+ throw new DataValidationException("Invalid entity data");
+ }
+
+ EntityType entityType = exportData.getEntityType();
+ EntityImportService> importService = getImportService(entityType);
+
+ EntityImportResult importResult = importService.importEntity(ctx, exportData);
+ ctx.putInternalId(exportData.getExternalId(), importResult.getSavedEntity().getId());
+
+ ctx.addReferenceCallback(importResult.getSaveReferencesCallback());
+ ctx.addEventCallback(importResult.getSendEventsCallback());
+ return importResult;
+ }
+
+ @Override
+ public void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException {
+ for (ThrowingRunnable saveReferencesCallback : ctx.getReferenceCallbacks()) {
+ saveReferencesCallback.run();
+ }
+
+ relationService.saveRelations(ctx.getTenantId(), new ArrayList<>(ctx.getRelations()));
+
+ for (EntityRelation relation : ctx.getRelations()) {
+ entityActionService.logEntityAction(ctx.getUser(), relation.getFrom(), null, null,
+ ActionType.RELATION_ADD_OR_UPDATE, null, relation);
+ entityActionService.logEntityAction(ctx.getUser(), relation.getTo(), null, null,
+ ActionType.RELATION_ADD_OR_UPDATE, null, relation);
+ }
+ }
+
+
+ @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);
+ if (exportService == null) {
+ throw new IllegalArgumentException("Export for entity type " + entityType + " is not supported");
+ }
+ return (EntityExportService) exportService;
+ }
+
+ @SuppressWarnings("unchecked")
+ private , D extends EntityExportData> EntityImportService getImportService(EntityType entityType) {
+ EntityImportService, ?, ?> importService = importServices.get(entityType);
+ if (importService == null) {
+ throw new IllegalArgumentException("Import for entity type " + entityType + " is not supported");
+ }
+ return (EntityImportService) importService;
+ }
+
+ @Autowired
+ private void setExportServices(DefaultEntityExportService, ?, ?> defaultExportService,
+ Collection> exportServices) {
+ exportServices.stream()
+ .sorted(Comparator.comparing(exportService -> exportService.getSupportedEntityTypes().size(), Comparator.reverseOrder()))
+ .forEach(exportService -> {
+ exportService.getSupportedEntityTypes().forEach(entityType -> {
+ this.exportServices.put(entityType, exportService);
+ });
+ });
+ SUPPORTED_ENTITY_TYPES.forEach(entityType -> {
+ this.exportServices.putIfAbsent(entityType, defaultExportService);
+ });
+ }
+
+ @Autowired
+ private void setImportServices(Collection> importServices) {
+ importServices.forEach(entityImportService -> {
+ this.importServices.put(entityImportService.getEntityType(), entityImportService);
+ });
+ }
+
+}
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
new file mode 100644
index 0000000000..8657528dac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/EntitiesExportImportService.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Comparator;
+
+public interface EntitiesExportImportService {
+
+ , I extends EntityId> EntityExportData exportEntity(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException;
+
+ , 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/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
new file mode 100644
index 0000000000..6e06144ae2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
@@ -0,0 +1,210 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+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.HasId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.ExportableEntityDao;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.permission.AccessControlService;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultExportableEntitiesService implements ExportableEntitiesService {
+
+ private final Map> daos = new HashMap<>();
+ private final Map> removers = new HashMap<>();
+
+ private final AccessControlService accessControlService;
+
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId) {
+ EntityType entityType = externalId.getEntityType();
+ Dao dao = getDao(entityType);
+
+ E entity = null;
+
+ if (dao instanceof ExportableEntityDao) {
+ ExportableEntityDao exportableEntityDao = (ExportableEntityDao) dao;
+ entity = exportableEntityDao.findByTenantIdAndExternalId(tenantId.getId(), externalId.getId());
+ }
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id) {
+ E entity = findEntityById(id);
+
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityById(I id) {
+ EntityType entityType = id.getEntityType();
+ Dao dao = getDao(entityType);
+ if (dao == null) {
+ throw new IllegalArgumentException("Unsupported entity type " + entityType);
+ }
+
+ return dao.findById(TenantId.SYS_TENANT_ID, id.getId());
+ }
+
+ @Override
+ public , I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name) {
+ Dao dao = getDao(entityType);
+
+ E entity = null;
+
+ if (dao instanceof ExportableEntityDao) {
+ ExportableEntityDao exportableEntityDao = (ExportableEntityDao) dao;
+ try {
+ entity = exportableEntityDao.findByTenantIdAndName(tenantId.getId(), name);
+ } catch (UnsupportedOperationException ignored) {
+ }
+ }
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+
+ return entity;
+ }
+
+ @Override
+ public , I extends EntityId> PageData findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink) {
+ ExportableEntityDao dao = getExportableEntityDao(entityType);
+ if (dao != null) {
+ return dao.findByTenantId(tenantId.getId(), pageLink);
+ } else {
+ return new PageData<>();
+ }
+ }
+
+ @Override
+ public I getExternalIdByInternal(I internalId) {
+ ExportableEntityDao dao = getExportableEntityDao(internalId.getEntityType());
+ if (dao != null) {
+ return dao.getExternalIdByInternal(internalId);
+ } else {
+ return null;
+ }
+ }
+
+ private boolean belongsToTenant(HasId extends EntityId> entity, TenantId tenantId) {
+ return tenantId.equals(((HasTenantId) entity).getTenantId());
+ }
+
+
+ @Override
+ public void removeById(TenantId tenantId, I id) {
+ EntityType entityType = id.getEntityType();
+ BiConsumer entityRemover = removers.get(entityType);
+ if (entityRemover == null) {
+ throw new IllegalArgumentException("Unsupported entity type " + entityType);
+ }
+ entityRemover.accept(tenantId, id);
+ }
+
+ private > ExportableEntityDao getExportableEntityDao(EntityType entityType) {
+ Dao dao = getDao(entityType);
+ if (dao instanceof ExportableEntityDao) {
+ return (ExportableEntityDao) dao;
+ } else {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Dao getDao(EntityType entityType) {
+ return (Dao) daos.get(entityType);
+ }
+
+ @Autowired
+ private void setDaos(Collection> daos) {
+ daos.forEach(dao -> {
+ if (dao.getEntityType() != null) {
+ this.daos.put(dao.getEntityType(), dao);
+ }
+ });
+ }
+
+ @Autowired
+ private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService,
+ DashboardService dashboardService, DeviceProfileService deviceProfileService,
+ DeviceService deviceService, WidgetsBundleService widgetsBundleService) {
+ removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> {
+ customerService.deleteCustomer(tenantId, (CustomerId) entityId);
+ });
+ removers.put(EntityType.ASSET, (tenantId, entityId) -> {
+ assetService.deleteAsset(tenantId, (AssetId) entityId);
+ });
+ removers.put(EntityType.RULE_CHAIN, (tenantId, entityId) -> {
+ ruleChainService.deleteRuleChainById(tenantId, (RuleChainId) entityId);
+ });
+ removers.put(EntityType.DASHBOARD, (tenantId, entityId) -> {
+ dashboardService.deleteDashboard(tenantId, (DashboardId) entityId);
+ });
+ removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> {
+ deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId);
+ });
+ removers.put(EntityType.DEVICE, (tenantId, entityId) -> {
+ deviceService.deleteDevice(tenantId, (DeviceId) entityId);
+ });
+ removers.put(EntityType.WIDGETS_BUNDLE, (tenantId, entityId) -> {
+ widgetsBundleService.deleteWidgetsBundle(tenantId, (WidgetsBundleId) entityId);
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java
new file mode 100644
index 0000000000..505d1b2a3f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/EntityExportService.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+public interface EntityExportService, D extends EntityExportData> {
+
+ D getExportData(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
new file mode 100644
index 0000000000..0a99979e8c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/ExportableEntitiesService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+
+public interface ExportableEntitiesService {
+
+ , I extends EntityId> E findEntityByTenantIdAndExternalId(TenantId tenantId, I externalId);
+
+ , I extends EntityId> E findEntityByTenantIdAndId(TenantId tenantId, I id);
+
+ , I extends EntityId> E findEntityById(I id);
+
+ , I extends EntityId> E findEntityByTenantIdAndName(TenantId tenantId, EntityType entityType, String name);
+
+ , I extends EntityId> PageData findEntitiesByTenantId(TenantId tenantId, EntityType entityType, PageLink pageLink);
+
+ I getExternalIdByInternal(I internalId);
+
+ void removeById(TenantId tenantId, I id);
+
+}
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
new file mode 100644
index 0000000000..8875491d42
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2022 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.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+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 AssetExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Asset asset, EntityExportData exportData) {
+ asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.ASSET);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
new file mode 100644
index 0000000000..169b7f273c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/BaseEntityExportService.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+public abstract class BaseEntityExportService, D extends EntityExportData> extends DefaultEntityExportService {
+
+ @Override
+ protected void setAdditionalExportData(EntitiesExportCtx> ctx, E entity, D exportData) throws ThingsboardException {
+ setRelatedEntities(ctx, entity, (D) exportData);
+ super.setAdditionalExportData(ctx, entity, exportData);
+ }
+
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, E mainEntity, D exportData) {
+ }
+
+ protected D newExportData() {
+ return (D) new EntityExportData();
+ }
+
+ ;
+
+ public abstract Set getSupportedEntityTypes();
+
+ protected void replaceUuidsRecursively(EntitiesExportCtx> ctx, JsonNode node, Set skipFieldsSet) {
+ JacksonUtil.replaceUuidsRecursively(node, skipFieldsSet, uuid -> getExternalIdOrElseInternalByUuid(ctx, uuid));
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
new file mode 100644
index 0000000000..9a3b195b57
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DashboardExportService.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Lists;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DashboardId;
+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 org.thingsboard.common.util.RegexUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@TbCoreComponent
+public class DashboardExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Dashboard dashboard, EntityExportData exportData) {
+ if (CollectionUtils.isNotEmpty(dashboard.getAssignedCustomers())) {
+ dashboard.getAssignedCustomers().forEach(customerInfo -> {
+ customerInfo.setCustomerId(getExternalIdOrElseInternal(ctx, customerInfo.getCustomerId()));
+ });
+ }
+ for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
+ replaceUuidsRecursively(ctx, entityAlias, Collections.emptySet());
+ }
+ for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
+ replaceUuidsRecursively(ctx, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"));
+ }
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DASHBOARD);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
new file mode 100644
index 0000000000..7e236370df
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright © 2016-2022 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.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@Primary
+public class DefaultEntityExportService, D extends EntityExportData> implements EntityExportService {
+
+ @Autowired
+ @Lazy
+ protected ExportableEntitiesService exportableEntitiesService;
+ @Autowired
+ private RelationService relationService;
+ @Autowired
+ private AttributesService attributesService;
+
+ @Override
+ public final D getExportData(EntitiesExportCtx> ctx, I entityId) throws ThingsboardException {
+ D exportData = newExportData();
+
+ E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId);
+ if (entity == null) {
+ throw new IllegalArgumentException(entityId.getEntityType() + " [" + entityId.getId() + "] not found");
+ }
+
+ exportData.setEntity(entity);
+ exportData.setEntityType(entityId.getEntityType());
+ setAdditionalExportData(ctx, entity, exportData);
+
+ var externalId = entity.getExternalId() != null ? entity.getExternalId() : entity.getId();
+ ctx.putExternalId(entityId, externalId);
+ entity.setId(externalId);
+ entity.setTenantId(null);
+
+ return exportData;
+ }
+
+ protected void setAdditionalExportData(EntitiesExportCtx> ctx, E entity, D exportData) throws ThingsboardException {
+ var exportSettings = ctx.getSettings();
+ if (exportSettings.isExportRelations()) {
+ List relations = exportRelations(ctx, entity);
+ relations.forEach(relation -> {
+ relation.setFrom(getExternalIdOrElseInternal(ctx, relation.getFrom()));
+ relation.setTo(getExternalIdOrElseInternal(ctx, relation.getTo()));
+ });
+ exportData.setRelations(relations);
+ }
+ if (exportSettings.isExportAttributes()) {
+ Map> attributes = exportAttributes(ctx, entity);
+ exportData.setAttributes(attributes);
+ }
+ }
+
+ private List exportRelations(EntitiesExportCtx> ctx, E entity) throws ThingsboardException {
+ List relations = new ArrayList<>();
+
+ List inboundRelations = relationService.findByTo(ctx.getTenantId(), entity.getId(), RelationTypeGroup.COMMON);
+ relations.addAll(inboundRelations);
+
+ List outboundRelations = relationService.findByFrom(ctx.getTenantId(), entity.getId(), RelationTypeGroup.COMMON);
+ relations.addAll(outboundRelations);
+ return relations;
+ }
+
+ private Map> exportAttributes(EntitiesExportCtx> ctx, E entity) throws ThingsboardException {
+ List scopes;
+ if (entity.getId().getEntityType() == EntityType.DEVICE) {
+ scopes = List.of(DataConstants.SERVER_SCOPE, DataConstants.SHARED_SCOPE);
+ } else {
+ scopes = Collections.singletonList(DataConstants.SERVER_SCOPE);
+ }
+ Map> attributes = new LinkedHashMap<>();
+ scopes.forEach(scope -> {
+ try {
+ attributes.put(scope, attributesService.findAll(ctx.getTenantId(), entity.getId(), scope).get().stream()
+ .map(attribute -> {
+ AttributeExportData attributeExportData = new AttributeExportData();
+ attributeExportData.setKey(attribute.getKey());
+ attributeExportData.setLastUpdateTs(attribute.getLastUpdateTs());
+ attributeExportData.setStrValue(attribute.getStrValue().orElse(null));
+ attributeExportData.setDoubleValue(attribute.getDoubleValue().orElse(null));
+ attributeExportData.setLongValue(attribute.getLongValue().orElse(null));
+ attributeExportData.setBooleanValue(attribute.getBooleanValue().orElse(null));
+ attributeExportData.setJsonValue(attribute.getJsonValue().orElse(null));
+ return attributeExportData;
+ })
+ .collect(Collectors.toList()));
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return attributes;
+ }
+
+ protected ID getExternalIdOrElseInternal(EntitiesExportCtx> ctx, ID internalId) {
+ if (internalId == null || internalId.isNullUid()) return internalId;
+ var result = ctx.getExternalId(internalId);
+ if (result == null) {
+ result = Optional.ofNullable(exportableEntitiesService.getExternalIdByInternal(internalId))
+ .orElse(internalId);
+ ctx.putExternalId(internalId, result);
+ }
+ return result;
+ }
+
+ protected UUID getExternalIdOrElseInternalByUuid(EntitiesExportCtx> ctx, UUID internalUuid) {
+ for (EntityType entityType : EntityType.values()) {
+ EntityId internalId;
+ try {
+ internalId = EntityIdFactory.getByTypeAndUuid(entityType, internalUuid);
+ } catch (Exception e) {
+ continue;
+ }
+ EntityId externalId = ctx.getExternalId(internalId);
+ if (externalId != null) {
+ return externalId.getId();
+ }
+ }
+ for (EntityType entityType : EntityType.values()) {
+ EntityId internalId;
+ try {
+ internalId = EntityIdFactory.getByTypeAndUuid(entityType, internalUuid);
+ } catch (Exception e) {
+ continue;
+ }
+ EntityId externalId = exportableEntitiesService.getExternalIdByInternal(internalId);
+ if (externalId != null) {
+ ctx.putExternalId(internalId, externalId);
+ return externalId.getId();
+ }
+ }
+ return internalUuid;
+ }
+
+ protected D newExportData() {
+ return (D) new EntityExportData();
+ }
+
+}
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
new file mode 100644
index 0000000000..bc5136bee6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2022 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 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.device.DeviceCredentialsService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceExportService extends BaseEntityExportService {
+
+ private final DeviceCredentialsService deviceCredentialsService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, Device device, DeviceExportData exportData) {
+ device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
+ device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
+ if (ctx.getSettings().isExportCredentials()) {
+ var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
+ credentials.setId(null);
+ credentials.setDeviceId(null);
+ exportData.setCredentials(credentials);
+ }
+ }
+
+ @Override
+ protected DeviceExportData newExportData() {
+ return new DeviceExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DEVICE);
+ }
+
+}
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
new file mode 100644
index 0000000000..46423c66ef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2022 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.DeviceProfile;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+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 DeviceProfileExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, DeviceProfile deviceProfile, EntityExportData exportData) {
+ deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
+ deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.DEVICE_PROFILE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java
new file mode 100644
index 0000000000..3fff890d3a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/EntityViewExportService.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2022 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.EntityView;
+import org.thingsboard.server.common.data.id.EntityViewId;
+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 EntityViewExportService extends BaseEntityExportService> {
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, EntityView entityView, EntityExportData exportData) {
+ entityView.setEntityId(getExternalIdOrElseInternal(ctx, entityView.getEntityId()));
+ entityView.setCustomerId(getExternalIdOrElseInternal(ctx, entityView.getCustomerId()));
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.ENTITY_VIEW);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
new file mode 100644
index 0000000000..802a03ab94
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/RuleChainExportService.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class RuleChainExportService extends BaseEntityExportService {
+
+ private final RuleChainService ruleChainService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, RuleChain ruleChain, RuleChainExportData exportData) {
+ RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), ruleChain.getId());
+ Optional.ofNullable(metaData.getNodes()).orElse(Collections.emptyList())
+ .forEach(ruleNode -> {
+ ruleNode.setRuleChainId(null);
+ ctx.putExternalId(ruleNode.getId(), ruleNode.getExternalId());
+ ruleNode.setId(ctx.getExternalId(ruleNode.getId()));
+ ruleNode.setCreatedTime(0);
+ ruleNode.setExternalId(null);
+ replaceUuidsRecursively(ctx, ruleNode.getConfiguration(), Collections.emptySet());
+ });
+ Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList())
+ .forEach(ruleChainConnectionInfo -> {
+ ruleChainConnectionInfo.setTargetRuleChainId(getExternalIdOrElseInternal(ctx, ruleChainConnectionInfo.getTargetRuleChainId()));
+ });
+ exportData.setMetaData(metaData);
+ if (ruleChain.getFirstRuleNodeId() != null) {
+ ruleChain.setFirstRuleNodeId(ctx.getExternalId(ruleChain.getFirstRuleNodeId()));
+ }
+ }
+
+ @Override
+ protected RuleChainExportData newExportData() {
+ return new RuleChainExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.RULE_CHAIN);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
new file mode 100644
index 0000000000..d3dd23910f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/WidgetsBundleExportService.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2022 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 lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData;
+import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+
+import java.util.List;
+import java.util.Set;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class WidgetsBundleExportService extends BaseEntityExportService {
+
+ private final WidgetTypeService widgetTypeService;
+
+ @Override
+ protected void setRelatedEntities(EntitiesExportCtx> ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData) {
+ if (widgetsBundle.getTenantId() == null || widgetsBundle.getTenantId().isNullUid()) {
+ throw new IllegalArgumentException("Export of system Widget Bundles is not allowed");
+ }
+
+ List widgets = widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(ctx.getTenantId(), widgetsBundle.getAlias());
+ exportData.setWidgets(widgets);
+ }
+
+ @Override
+ protected WidgetsBundleExportData newExportData() {
+ return new WidgetsBundleExportData();
+ }
+
+ @Override
+ public Set getSupportedEntityTypes() {
+ return Set.of(EntityType.WIDGETS_BUNDLE);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java
new file mode 100644
index 0000000000..9437f21e60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/EntityImportService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2022 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;
+
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+public interface EntityImportService, D extends EntityExportData> {
+
+ EntityImportResult importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException;
+
+ EntityType getEntityType();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
similarity index 97%
rename from application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
index 81a0068a4d..b92e100401 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonObject;
@@ -44,7 +44,6 @@ import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.action.EntityActionService;
-import org.thingsboard.server.service.importing.BulkImportRequest.ColumnMapping;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@@ -67,7 +66,6 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -165,7 +163,7 @@ public abstract class AbstractBulkImportService data) {
+ private void saveKvs(SecurityUser user, E entity, Map data) {
Arrays.stream(BulkImportColumnType.values())
.filter(BulkImportColumnType::isKv)
.map(kvType -> {
@@ -249,7 +247,7 @@ public abstract class AbstractBulkImportService columnsMappings = request.getMapping().getColumns();
+ List columnsMappings = request.getMapping().getColumns();
return records.stream()
.map(record -> {
EntityData entityData = new EntityData();
@@ -280,7 +278,7 @@ public abstract class AbstractBulkImportService fields = new LinkedHashMap<>();
- private final Map kvs = new LinkedHashMap<>();
+ private final Map kvs = new LinkedHashMap<>();
private int lineNumber;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
similarity index 97%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
index 96075eb4c8..24b566e631 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportColumnType.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Getter;
import org.thingsboard.server.common.data.DataConstants;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
index 9f8195ca45..e8eac6a9ed 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportRequest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
index 651aedeb0b..0626c8e690 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/BulkImportResult.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
similarity index 92%
rename from application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
index 45c2551be2..d48e9a3d23 100644
--- a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/ImportedEntityInfo.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.importing;
+package org.thingsboard.server.service.sync.ie.importing.csv;
import lombok.Data;
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
new file mode 100644
index 0000000000..cd9dc17d60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright © 2016-2022 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.asset.Asset;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+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.dao.asset.AssetService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class AssetImportService extends BaseEntityImportService> {
+
+ private final AssetService assetService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Asset asset, IdProvider idProvider) {
+ asset.setTenantId(tenantId);
+ asset.setCustomerId(idProvider.getInternalId(asset.getCustomerId()));
+ }
+
+ @Override
+ protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, EntityExportData exportData, IdProvider idProvider) {
+ return asset;
+ }
+
+ @Override
+ protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider) {
+ return assetService.saveAsset(asset);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Asset savedAsset, Asset oldAsset) throws ThingsboardException {
+ super.onEntitySaved(user, savedAsset, oldAsset);
+ if (oldAsset != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected Asset deepCopy(Asset asset) {
+ return new Asset(asset);
+ }
+
+ @Override
+ protected void cleanupForComparison(Asset e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ASSET;
+ }
+
+}
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
new file mode 100644
index 0000000000..26c5a869f9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
@@ -0,0 +1,398 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import com.google.api.client.util.Objects;
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.JsonDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.service.action.EntityActionService;
+import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.ie.importing.EntityImportService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+public abstract class BaseEntityImportService, D extends EntityExportData> implements EntityImportService {
+
+ @Autowired
+ @Lazy
+ private ExportableEntitiesService exportableEntitiesService;
+ @Autowired
+ private RelationService relationService;
+ @Autowired
+ private TelemetrySubscriptionService tsSubService;
+ @Autowired
+ protected EntityActionService entityActionService;
+ @Autowired
+ protected TbClusterService clusterService;
+ @Autowired
+ protected TbNotificationEntityService entityNotificationService;
+
+ @Transactional(rollbackFor = Exception.class)
+ @Override
+ public EntityImportResult importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException {
+ EntityImportResult importResult = new EntityImportResult<>();
+ ctx.setCurrentImportResult(importResult);
+ importResult.setEntityType(getEntityType());
+ IdProvider idProvider = new IdProvider(ctx, importResult);
+
+ E entity = exportData.getEntity();
+ entity.setExternalId(entity.getId());
+
+ E existingEntity = findExistingEntity(ctx, entity, idProvider);
+ importResult.setOldEntity(existingEntity);
+
+ setOwner(ctx.getTenantId(), entity, idProvider);
+ if (existingEntity == null) {
+ entity.setId(null);
+ } else {
+ entity.setId(existingEntity.getId());
+ entity.setCreatedTime(existingEntity.getCreatedTime());
+ }
+
+ E prepared = prepare(ctx, entity, existingEntity, exportData, idProvider);
+
+ boolean saveOrUpdate = existingEntity == null || compare(ctx, exportData, prepared, existingEntity);
+
+ if (saveOrUpdate) {
+ E savedEntity = saveOrUpdate(ctx, prepared, exportData, idProvider);
+ boolean created = existingEntity == null;
+ importResult.setCreated(created);
+ importResult.setUpdated(!created);
+ importResult.setSavedEntity(savedEntity);
+ ctx.putInternalId(exportData.getExternalId(), savedEntity.getId());
+ } else {
+ importResult.setSavedEntity(existingEntity);
+ ctx.putInternalId(exportData.getExternalId(), existingEntity.getId());
+ importResult.setUpdatedRelatedEntities(updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider));
+ }
+
+ processAfterSaved(ctx, importResult, exportData, idProvider);
+
+ return importResult;
+ }
+
+ protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
+ return false;
+ }
+
+ @Override
+ public abstract EntityType getEntityType();
+
+ protected abstract void setOwner(TenantId tenantId, E entity, IdProvider idProvider);
+
+ 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) {
+ var newCopy = deepCopy(prepared);
+ var existingCopy = deepCopy(existing);
+ cleanupForComparison(newCopy);
+ cleanupForComparison(existingCopy);
+ var result = !newCopy.equals(existingCopy);
+ if (result) {
+ log.debug("[{}] Found update.", prepared.getId());
+ log.debug("[{}] From: {}", prepared.getId(), newCopy);
+ log.debug("[{}] To: {}", prepared.getId(), existingCopy);
+ }
+ return result;
+ }
+
+ protected abstract E deepCopy(E e);
+
+ protected void cleanupForComparison(E e) {
+ e.setTenantId(null);
+ e.setCreatedTime(0);
+ }
+
+ protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider);
+
+
+ protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
+ E savedEntity = importResult.getSavedEntity();
+ E oldEntity = importResult.getOldEntity();
+
+ if (importResult.isCreated() || importResult.isUpdated()) {
+ importResult.addSendEventsCallback(() -> onEntitySaved(ctx.getUser(), savedEntity, oldEntity));
+ }
+
+ if (ctx.isUpdateRelations() && exportData.getRelations() != null) {
+ importRelations(ctx, exportData.getRelations(), importResult, idProvider);
+ }
+ if (ctx.isSaveAttributes() && exportData.getAttributes() != null) {
+ if (exportData.getAttributes().values().stream().anyMatch(d -> !d.isEmpty())) {
+ importResult.setUpdatedRelatedEntities(true);
+ }
+ importAttributes(ctx.getUser(), exportData.getAttributes(), importResult);
+ }
+ }
+
+ private void importRelations(EntitiesImportCtx ctx, List relations, EntityImportResult importResult, IdProvider idProvider) {
+ var tenantId = ctx.getTenantId();
+ E entity = importResult.getSavedEntity();
+ importResult.addSaveReferencesCallback(() -> {
+ for (EntityRelation relation : relations) {
+ if (!relation.getTo().equals(entity.getId())) {
+ relation.setTo(idProvider.getInternalId(relation.getTo()));
+ }
+ if (!relation.getFrom().equals(entity.getId())) {
+ relation.setFrom(idProvider.getInternalId(relation.getFrom()));
+ }
+ }
+
+ Map relationsMap = new LinkedHashMap<>();
+ relations.forEach(r -> relationsMap.put(r, r));
+
+ if (importResult.getOldEntity() != null) {
+ List existingRelations = new ArrayList<>();
+ existingRelations.addAll(relationService.findByTo(tenantId, entity.getId(), RelationTypeGroup.COMMON));
+ existingRelations.addAll(relationService.findByFrom(tenantId, entity.getId(), RelationTypeGroup.COMMON));
+
+ for (EntityRelation existingRelation : existingRelations) {
+ EntityRelation relation = relationsMap.get(existingRelation);
+ if (relation == null) {
+ importResult.setUpdatedRelatedEntities(true);
+ relationService.deleteRelation(tenantId, existingRelation);
+ importResult.addSendEventsCallback(() -> {
+ entityActionService.logEntityAction(ctx.getUser(), existingRelation.getFrom(), null, null,
+ ActionType.RELATION_DELETED, null, existingRelation);
+ entityActionService.logEntityAction(ctx.getUser(), existingRelation.getTo(), null, null,
+ ActionType.RELATION_DELETED, null, existingRelation);
+ });
+ } else if (Objects.equal(relation.getAdditionalInfo(), existingRelation.getAdditionalInfo())) {
+ relationsMap.remove(relation);
+ }
+ }
+ }
+ if (!relationsMap.isEmpty()) {
+ importResult.setUpdatedRelatedEntities(true);
+ ctx.addRelations(relationsMap.values());
+ }
+ });
+ }
+
+ private void importAttributes(SecurityUser user, Map> attributes, EntityImportResult importResult) {
+ E entity = importResult.getSavedEntity();
+ importResult.addSaveReferencesCallback(() -> {
+ attributes.forEach((scope, attributesExportData) -> {
+ List attributeKvEntries = attributesExportData.stream()
+ .map(attributeExportData -> {
+ KvEntry kvEntry;
+ String key = attributeExportData.getKey();
+ if (attributeExportData.getStrValue() != null) {
+ kvEntry = new StringDataEntry(key, attributeExportData.getStrValue());
+ } else if (attributeExportData.getBooleanValue() != null) {
+ kvEntry = new BooleanDataEntry(key, attributeExportData.getBooleanValue());
+ } else if (attributeExportData.getDoubleValue() != null) {
+ kvEntry = new DoubleDataEntry(key, attributeExportData.getDoubleValue());
+ } else if (attributeExportData.getLongValue() != null) {
+ kvEntry = new LongDataEntry(key, attributeExportData.getLongValue());
+ } else if (attributeExportData.getJsonValue() != null) {
+ kvEntry = new JsonDataEntry(key, attributeExportData.getJsonValue());
+ } else {
+ throw new IllegalArgumentException("Invalid attribute export data");
+ }
+ return new BaseAttributeKvEntry(kvEntry, attributeExportData.getLastUpdateTs());
+ })
+ .collect(Collectors.toList());
+ // fixme: attributes are saved outside the transaction
+ tsSubService.saveAndNotify(user.getTenantId(), entity.getId(), scope, attributeKvEntries, new FutureCallback() {
+ @Override
+ public void onSuccess(@Nullable Void unused) {
+ }
+
+ @Override
+ public void onFailure(Throwable thr) {
+ log.error("Failed to import attributes for {} {}", entity.getId().getEntityType(), entity.getId(), thr);
+ }
+ });
+ });
+ });
+ }
+
+ protected void onEntitySaved(SecurityUser user, E savedEntity, E oldEntity) throws ThingsboardException {
+ entityActionService.logEntityAction(user, savedEntity.getId(), savedEntity,
+ savedEntity instanceof HasCustomerId ? ((HasCustomerId) savedEntity).getCustomerId() : user.getCustomerId(),
+ oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, null);
+ }
+
+
+ @SuppressWarnings("unchecked")
+ protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
+ return (E) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
+ .or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entity.getId())))
+ .or(() -> {
+ if (ctx.isFindExistingByName()) {
+ return Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndName(ctx.getTenantId(), getEntityType(), entity.getName()));
+ } else {
+ return Optional.empty();
+ }
+ })
+ .orElse(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private HasId findInternalEntity(TenantId tenantId, ID externalId) {
+ return (HasId) Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndExternalId(tenantId, externalId))
+ .or(() -> Optional.ofNullable(exportableEntitiesService.findEntityByTenantIdAndId(tenantId, externalId)))
+ .orElseThrow(() -> new MissingEntityException(externalId));
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @RequiredArgsConstructor
+ protected class IdProvider {
+ private final EntitiesImportCtx ctx;
+ private final EntityImportResult importResult;
+
+ public ID getInternalId(ID externalId) {
+ return getInternalId(externalId, true);
+ }
+
+ public ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
+ if (externalId == null || externalId.isNullUid()) return null;
+
+ EntityId localId = ctx.getInternalId(externalId);
+ if (localId != null) {
+ return (ID) localId;
+ }
+
+ HasId entity;
+ try {
+ entity = findInternalEntity(ctx.getTenantId(), externalId);
+ } catch (Exception e) {
+ if (throwExceptionIfNotFound) {
+ throw e;
+ } else {
+ importResult.setUpdatedAllExternalIds(false);
+ return null;
+ }
+ }
+ ctx.putInternalId(externalId, entity.getId());
+ return entity.getId();
+ }
+
+ public Optional getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set hints) {
+ if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
+
+ for (EntityType entityType : EntityType.values()) {
+ Optional externalIdOpt = buildEntityId(entityType, externalUuid);
+ if (!externalIdOpt.isPresent()) {
+ continue;
+ }
+ EntityId internalId = ctx.getInternalId(externalIdOpt.get());
+ if (internalId != null) {
+ return Optional.of(internalId);
+ }
+ }
+
+ if (fetchAllUUIDs) {
+ for (EntityType entityType : hints) {
+ Optional internalId = lookupInDb(externalUuid, entityType);
+ if (internalId.isPresent()) return internalId;
+ }
+ for (EntityType entityType : EntityType.values()) {
+ if (hints.contains(entityType)) {
+ continue;
+ }
+ Optional internalId = lookupInDb(externalUuid, entityType);
+ if (internalId.isPresent()) return internalId;
+ }
+ }
+
+ importResult.setUpdatedAllExternalIds(false);
+ return Optional.empty();
+ }
+
+ private Optional lookupInDb(UUID externalUuid, EntityType entityType) {
+ Optional externalIdOpt = buildEntityId(entityType, externalUuid);
+ if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) {
+ return Optional.empty();
+ }
+ EntityId internalId = getInternalId(externalIdOpt.get(), false);
+ if (internalId != null) {
+ return Optional.of(internalId);
+ } else {
+ ctx.registerNotFound(externalIdOpt.get());
+ }
+ return Optional.empty();
+ }
+
+ private Optional buildEntityId(EntityType entityType, UUID externalUuid) {
+ try {
+ return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ }
+
+ protected T getOldEntityField(O oldEntity, Function getter) {
+ return oldEntity == null ? null : getter.apply(oldEntity);
+ }
+
+ protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode entityAlias, Set skipFieldsSet, LinkedHashSet hints) {
+ JacksonUtil.replaceUuidsRecursively(entityAlias, skipFieldsSet,
+ uuid -> idProvider.getInternalIdByUuid(uuid, ctx.isFinalImportAttempt(), hints).map(EntityId::getId).orElse(uuid));
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
new file mode 100644
index 0000000000..1dc476340f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2022 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.Customer;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.customer.CustomerDao;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class CustomerImportService extends BaseEntityImportService> {
+
+ private final CustomerService customerService;
+ private final CustomerDao customerDao;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Customer customer, IdProvider idProvider) {
+ customer.setTenantId(tenantId);
+ }
+
+ @Override
+ protected Customer prepare(EntitiesImportCtx ctx, Customer customer, Customer old, EntityExportData exportData, IdProvider idProvider) {
+ if (customer.isPublic()) {
+ Customer publicCustomer = customerService.findOrCreatePublicCustomer(ctx.getTenantId());
+ publicCustomer.setExternalId(customer.getExternalId());
+ return publicCustomer;
+ } else {
+ return customer;
+ }
+ }
+
+ @Override
+ protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData exportData, IdProvider idProvider) {
+ if (!customer.isPublic()) {
+ return customerService.saveCustomer(customer);
+ } else {
+ return customerDao.save(ctx.getTenantId(), customer);
+ }
+ }
+
+ @Override
+ protected Customer deepCopy(Customer customer) {
+ return new Customer(customer);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Customer savedCustomer, Customer oldCustomer) throws ThingsboardException {
+ super.onEntitySaved(user, savedCustomer, oldCustomer);
+ if (oldCustomer != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedCustomer.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.CUSTOMER;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
new file mode 100644
index 0000000000..b6de84bfff
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Lists;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ShortCustomerInfo;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+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.dashboard.DashboardService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DashboardImportService extends BaseEntityImportService> {
+
+ private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.DASHBOARD, EntityType.DEVICE, EntityType.ASSET));
+
+ private final DashboardService dashboardService;
+
+
+ @Override
+ protected void setOwner(TenantId tenantId, Dashboard dashboard, IdProvider idProvider) {
+ dashboard.setTenantId(tenantId);
+ }
+
+ @Override
+ protected Dashboard findExistingEntity(EntitiesImportCtx ctx, Dashboard dashboard, IdProvider idProvider) {
+ Dashboard existingDashboard = super.findExistingEntity(ctx, dashboard, idProvider);
+ if (existingDashboard == null && ctx.isFindExistingByName()) {
+ existingDashboard = dashboardService.findTenantDashboardsByTitle(ctx.getTenantId(), dashboard.getName()).stream().findFirst().orElse(null);
+ }
+ return existingDashboard;
+ }
+
+ @Override
+ protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData exportData, IdProvider idProvider) {
+ for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
+ replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS);
+ }
+ for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
+ replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS);
+ }
+ return dashboard;
+ }
+
+ @Override
+ protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData exportData, IdProvider idProvider) {
+ var tenantId = ctx.getTenantId();
+
+ Set assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream()
+ .peek(customerInfo -> customerInfo.setCustomerId(idProvider.getInternalId(customerInfo.getCustomerId())))
+ .collect(Collectors.toSet());
+
+ if (dashboard.getId() == null) {
+ dashboard.setAssignedCustomers(assignedCustomers);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ for (ShortCustomerInfo customerInfo : assignedCustomers) {
+ dashboard = dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerInfo.getCustomerId());
+ }
+ } else {
+ Set existingAssignedCustomers = Optional.ofNullable(dashboardService.findDashboardById(tenantId, dashboard.getId()).getAssignedCustomers())
+ .orElse(Collections.emptySet()).stream().map(ShortCustomerInfo::getCustomerId).collect(Collectors.toSet());
+ Set newAssignedCustomers = assignedCustomers.stream().map(ShortCustomerInfo::getCustomerId).collect(Collectors.toSet());
+
+ Set toUnassign = new HashSet<>(existingAssignedCustomers);
+ toUnassign.removeAll(newAssignedCustomers);
+ for (CustomerId customerId : toUnassign) {
+ assignedCustomers = dashboardService.unassignDashboardFromCustomer(tenantId, dashboard.getId(), customerId).getAssignedCustomers();
+ }
+
+ Set toAssign = new HashSet<>(newAssignedCustomers);
+ toAssign.removeAll(existingAssignedCustomers);
+ for (CustomerId customerId : toAssign) {
+ assignedCustomers = dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customerId).getAssignedCustomers();
+ }
+ dashboard.setAssignedCustomers(assignedCustomers);
+ dashboard = dashboardService.saveDashboard(dashboard);
+ }
+ return dashboard;
+ }
+
+ @Override
+ protected Dashboard deepCopy(Dashboard dashboard) {
+ return new Dashboard(dashboard);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Dashboard savedDashboard, Dashboard oldDashboard) throws ThingsboardException {
+ super.onEntitySaved(user, savedDashboard, oldDashboard);
+ if (oldDashboard != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedDashboard.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DASHBOARD;
+ }
+
+}
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
new file mode 100644
index 0000000000..c699f466d7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2022 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.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+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.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceImportService extends BaseEntityImportService {
+
+ private final DeviceService deviceService;
+ private final DeviceCredentialsService credentialsService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, Device device, IdProvider idProvider) {
+ device.setTenantId(tenantId);
+ device.setCustomerId(idProvider.getInternalId(device.getCustomerId()));
+ }
+
+ @Override
+ protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
+ device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
+ device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId));
+ device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId));
+ return device;
+ }
+
+ @Override
+ protected Device deepCopy(Device d) {
+ return new Device(d);
+ }
+
+ @Override
+ protected void cleanupForComparison(Device e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
+ if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
+ exportData.getCredentials().setId(null);
+ exportData.getCredentials().setDeviceId(null);
+ return deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
+ } else {
+ return deviceService.saveDevice(device);
+ }
+ }
+
+ @Override
+ protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, Device prepared, DeviceExportData exportData, IdProvider idProvider) {
+ boolean updated = super.updateRelatedEntitiesIfUnmodified(ctx, prepared, exportData, idProvider);
+ var credentials = exportData.getCredentials();
+ if (credentials != null && ctx.isSaveCredentials()) {
+ var existing = credentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), prepared.getId());
+ credentials.setId(existing.getId());
+ credentials.setDeviceId(prepared.getId());
+ if (!existing.equals(credentials)) {
+ credentialsService.updateDeviceCredentials(ctx.getTenantId(), credentials);
+ updated = true;
+ }
+ }
+ return updated;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, Device savedDevice, Device oldDevice) throws ThingsboardException {
+ super.onEntitySaved(user, savedDevice, oldDevice);
+ clusterService.onDeviceUpdated(savedDevice, oldDevice);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE;
+ }
+
+}
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
new file mode 100644
index 0000000000..0944792751
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016-2022 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.DeviceProfile;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.ota.OtaPackageStateService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Objects;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class DeviceProfileImportService extends BaseEntityImportService> {
+
+ private final DeviceProfileService deviceProfileService;
+ private final OtaPackageStateService otaPackageStateService;
+
+ @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) {
+ deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
+ deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
+ deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId));
+ deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId));
+ return deviceProfile;
+ }
+
+ @Override
+ protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData, IdProvider idProvider) {
+ return deviceProfileService.saveDeviceProfile(deviceProfile);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, DeviceProfile savedDeviceProfile, DeviceProfile oldDeviceProfile) throws ThingsboardException {
+ super.onEntitySaved(user, savedDeviceProfile, oldDeviceProfile);
+ clusterService.onDeviceProfileChange(savedDeviceProfile, null);
+ clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedDeviceProfile.getId(),
+ oldDeviceProfile == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedDeviceProfile.getId(),
+ oldDeviceProfile == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED);
+ otaPackageStateService.update(savedDeviceProfile,
+ oldDeviceProfile != null && !Objects.equals(oldDeviceProfile.getFirmwareId(), savedDeviceProfile.getFirmwareId()),
+ oldDeviceProfile != null && !Objects.equals(oldDeviceProfile.getSoftwareId(), savedDeviceProfile.getSoftwareId()));
+ }
+
+ @Override
+ protected DeviceProfile deepCopy(DeviceProfile deviceProfile) {
+ return new DeviceProfile(deviceProfile);
+ }
+
+ @Override
+ protected void cleanupForComparison(DeviceProfile deviceProfile) {
+ super.cleanupForComparison(deviceProfile);
+ deviceProfile.setFirmwareId(null);
+ deviceProfile.setSoftwareId(null);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.DEVICE_PROFILE;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java
new file mode 100644
index 0000000000..57d22cf7be
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016-2022 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.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityViewId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.dao.entityview.EntityViewService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.entityView.TbEntityViewService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class EntityViewImportService extends BaseEntityImportService> {
+
+ private final EntityViewService entityViewService;
+
+ @Lazy
+ @Autowired
+ private TbEntityViewService tbEntityViewService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, EntityView entityView, IdProvider idProvider) {
+ entityView.setTenantId(tenantId);
+ entityView.setCustomerId(idProvider.getInternalId(entityView.getCustomerId()));
+ }
+
+ @Override
+ protected EntityView prepare(EntitiesImportCtx ctx, EntityView entityView, EntityView old, EntityExportData exportData, IdProvider idProvider) {
+ entityView.setEntityId(idProvider.getInternalId(entityView.getEntityId()));
+ return entityView;
+ }
+
+ @Override
+ protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData exportData, IdProvider idProvider) {
+ return entityViewService.saveEntityView(entityView);
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, EntityView savedEntityView, EntityView oldEntityView) throws ThingsboardException {
+ tbEntityViewService.updateEntityViewAttributes(user, savedEntityView, oldEntityView);
+ super.onEntitySaved(user, savedEntityView, oldEntityView);
+ if (oldEntityView != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedEntityView.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected EntityView deepCopy(EntityView entityView) {
+ return new EntityView(entityView);
+ }
+
+ @Override
+ protected void cleanupForComparison(EntityView e) {
+ super.cleanupForComparison(e);
+ if (e.getCustomerId() != null && e.getCustomerId().isNullUid()) {
+ e.setCustomerId(null);
+ }
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.ENTITY_VIEW;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java
new file mode 100644
index 0000000000..1e869429d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ImportServiceException.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2022 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;
+
+public class ImportServiceException extends RuntimeException{
+ private static final long serialVersionUID = -4932715239522125041L;
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java
new file mode 100644
index 0000000000..a0a961bfef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/MissingEntityException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2022 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.Getter;
+import org.thingsboard.server.common.data.id.EntityId;
+
+public class MissingEntityException extends ImportServiceException {
+
+ private static final long serialVersionUID = 3669135386955906022L;
+ @Getter
+ private final EntityId entityId;
+
+ public MissingEntityException(EntityId entityId) {
+ this.entityId = entityId;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
new file mode 100644
index 0000000000..916c10844f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.rule.RuleChain;
+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.sync.ie.RuleChainExportData;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.rule.RuleNodeDao;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.common.util.RegexUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class RuleChainImportService extends BaseEntityImportService {
+
+ private static final LinkedHashSet HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET));
+
+ private final RuleChainService ruleChainService;
+ private final RuleNodeDao ruleNodeDao;
+
+ @Override
+ protected void setOwner(TenantId tenantId, RuleChain ruleChain, IdProvider idProvider) {
+ ruleChain.setTenantId(tenantId);
+ }
+
+ @Override
+ protected RuleChain findExistingEntity(EntitiesImportCtx ctx, RuleChain ruleChain, IdProvider idProvider) {
+ RuleChain existingRuleChain = super.findExistingEntity(ctx, ruleChain, idProvider);
+ if (existingRuleChain == null && ctx.isFindExistingByName()) {
+ existingRuleChain = ruleChainService.findTenantRuleChainsByTypeAndName(ctx.getTenantId(), ruleChain.getType(), ruleChain.getName()).stream().findFirst().orElse(null);
+ }
+ return existingRuleChain;
+ }
+
+ @Override
+ protected RuleChain prepare(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChain old, RuleChainExportData exportData, IdProvider idProvider) {
+ RuleChainMetaData metaData = exportData.getMetaData();
+ List ruleNodes = Optional.ofNullable(metaData.getNodes()).orElse(Collections.emptyList());
+ if (old != null) {
+ List nodeIds = ruleNodes.stream().map(RuleNode::getId).collect(Collectors.toList());
+ List existing = ruleNodeDao.findByExternalIds(old.getId(), nodeIds);
+ existing.forEach(node -> ctx.putInternalId(node.getExternalId(), node.getId()));
+ ruleNodes.forEach(node -> {
+ node.setRuleChainId(old.getId());
+ node.setExternalId(node.getId());
+ node.setId((RuleNodeId) ctx.getInternalId(node.getId()));
+ });
+ } else {
+ ruleNodes.forEach(node -> {
+ node.setRuleChainId(null);
+ node.setExternalId(node.getId());
+ node.setId(null);
+ });
+ }
+
+ ruleNodes.forEach(ruleNode -> replaceIdsRecursively(ctx, idProvider, ruleNode.getConfiguration(), Collections.emptySet(), HINTS));
+ Optional.ofNullable(metaData.getRuleChainConnections()).orElse(Collections.emptyList())
+ .forEach(ruleChainConnectionInfo -> {
+ ruleChainConnectionInfo.setTargetRuleChainId(idProvider.getInternalId(ruleChainConnectionInfo.getTargetRuleChainId(), false));
+ });
+ if (ruleChain.getFirstRuleNodeId() != null) {
+ ruleChain.setFirstRuleNodeId((RuleNodeId) ctx.getInternalId(ruleChain.getFirstRuleNodeId()));
+ }
+ return ruleChain;
+ }
+
+ @Override
+ protected RuleChain saveOrUpdate(EntitiesImportCtx ctx, RuleChain ruleChain, RuleChainExportData exportData, IdProvider idProvider) {
+ ruleChain = ruleChainService.saveRuleChain(ruleChain);
+ if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
+ exportData.getMetaData().setRuleChainId(ruleChain.getId());
+ ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData());
+ return ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChain.getId());
+ } else {
+ return ruleChain;
+ }
+ }
+
+ @Override
+ protected boolean compare(EntitiesImportCtx ctx, RuleChainExportData exportData, RuleChain prepared, RuleChain existing) {
+ boolean different = super.compare(ctx, exportData, prepared, existing);
+ if (!different) {
+ RuleChainMetaData newMD = exportData.getMetaData();
+ RuleChainMetaData existingMD = ruleChainService.loadRuleChainMetaData(ctx.getTenantId(), prepared.getId());
+ existingMD.setRuleChainId(null);
+ different = newMD.equals(existingMD);
+ }
+ return different;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, RuleChain savedRuleChain, RuleChain oldRuleChain) throws ThingsboardException {
+ super.onEntitySaved(user, savedRuleChain, oldRuleChain);
+ if (savedRuleChain.getType() == RuleChainType.CORE) {
+ clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedRuleChain.getId(),
+ oldRuleChain == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+ } else if (savedRuleChain.getType() == RuleChainType.EDGE && oldRuleChain != null) {
+ entityActionService.sendEntityNotificationMsgToEdgeService(user.getTenantId(), savedRuleChain.getId(), EdgeEventActionType.UPDATED);
+ }
+ }
+
+ @Override
+ protected RuleChain deepCopy(RuleChain ruleChain) {
+ return new RuleChain(ruleChain);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.RULE_CHAIN;
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java
new file mode 100644
index 0000000000..550f00952a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/WidgetsBundleImportService.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016-2022 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.edge.EdgeEventActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.sync.ie.WidgetsBundleExportData;
+import org.thingsboard.server.common.data.widget.BaseWidgetType;
+import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.widget.WidgetTypeService;
+import org.thingsboard.server.dao.widget.WidgetsBundleService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+public class WidgetsBundleImportService extends BaseEntityImportService {
+
+ private final WidgetsBundleService widgetsBundleService;
+ private final WidgetTypeService widgetTypeService;
+
+ @Override
+ protected void setOwner(TenantId tenantId, WidgetsBundle widgetsBundle, IdProvider idProvider) {
+ widgetsBundle.setTenantId(tenantId);
+ }
+
+ @Override
+ protected WidgetsBundle prepare(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundle old, WidgetsBundleExportData exportData, IdProvider idProvider) {
+ return widgetsBundle;
+ }
+
+ @Override
+ protected WidgetsBundle saveOrUpdate(EntitiesImportCtx ctx, WidgetsBundle widgetsBundle, WidgetsBundleExportData exportData, IdProvider idProvider) {
+ WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
+ if (widgetsBundle.getId() == null) {
+ for (WidgetTypeDetails widget : exportData.getWidgets()) {
+ widget.setId(null);
+ widget.setTenantId(ctx.getTenantId());
+ widget.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetTypeService.saveWidgetType(widget);
+ }
+ } else {
+ Map existingWidgets = widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(ctx.getTenantId(), savedWidgetsBundle.getAlias()).stream()
+ .collect(Collectors.toMap(BaseWidgetType::getAlias, w -> w));
+ for (WidgetTypeDetails widget : exportData.getWidgets()) {
+ WidgetTypeInfo existingWidget;
+ if ((existingWidget = existingWidgets.remove(widget.getAlias())) != null) {
+ widget.setId(existingWidget.getId());
+ widget.setCreatedTime(existingWidget.getCreatedTime());
+ } else {
+ widget.setId(null);
+ }
+ widget.setTenantId(ctx.getTenantId());
+ widget.setBundleAlias(savedWidgetsBundle.getAlias());
+ widgetTypeService.saveWidgetType(widget);
+ }
+ existingWidgets.values().stream()
+ .map(BaseWidgetType::getId)
+ .forEach(widgetTypeId -> widgetTypeService.deleteWidgetType(ctx.getTenantId(), widgetTypeId));
+ }
+ return savedWidgetsBundle;
+ }
+
+ @Override
+ protected void onEntitySaved(SecurityUser user, WidgetsBundle savedWidgetsBundle, WidgetsBundle oldWidgetsBundle) throws ThingsboardException {
+ super.onEntitySaved(user, savedWidgetsBundle, oldWidgetsBundle);
+ entityNotificationService.notifySendMsgToEdgeService(user.getTenantId(), savedWidgetsBundle.getId(),
+ oldWidgetsBundle == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED);
+ }
+
+ @Override
+ protected WidgetsBundle deepCopy(WidgetsBundle widgetsBundle) {
+ return new WidgetsBundle(widgetsBundle);
+ }
+
+ @Override
+ public EntityType getEntityType() {
+ return EntityType.WIDGETS_BUNDLE;
+ }
+
+}
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
new file mode 100644
index 0000000000..1f6c174fdc
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
@@ -0,0 +1,554 @@
+/**
+ * Copyright © 2016-2022 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.vc;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.common.util.TbStopWatch;
+import org.thingsboard.common.util.ThingsBoardExecutors;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.sync.ThrowingRunnable;
+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;
+import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
+import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
+import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
+import org.thingsboard.server.common.data.sync.vc.EntityLoadError;
+import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
+import org.thingsboard.server.common.data.sync.vc.EntityVersion;
+import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
+import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
+import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
+import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
+import org.thingsboard.server.common.data.sync.vc.request.create.AutoVersionCreateConfig;
+import org.thingsboard.server.common.data.sync.vc.request.create.ComplexVersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.create.EntityTypeVersionCreateConfig;
+import org.thingsboard.server.common.data.sync.vc.request.create.SingleEntityVersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.create.SyncStrategy;
+import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersionLoadRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig;
+import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.ie.EntitiesExportImportService;
+import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
+import org.thingsboard.server.service.sync.ie.importing.impl.MissingEntityException;
+import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
+import org.thingsboard.server.service.sync.vc.data.ComplexEntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
+import org.thingsboard.server.service.sync.vc.data.EntityTypeExportCtx;
+import org.thingsboard.server.service.sync.vc.data.ReimportTask;
+import org.thingsboard.server.service.sync.vc.data.SimpleEntitiesExportCtx;
+import org.thingsboard.server.service.sync.vc.repository.TbRepositorySettingsService;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+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 com.google.common.util.concurrent.Futures.transformAsync;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService {
+
+ private final TbRepositorySettingsService repositorySettingsService;
+ private final TbAutoCommitSettingsService autoCommitSettingsService;
+ private final GitVersionControlQueueService gitServiceQueue;
+ private final EntitiesExportImportService exportImportService;
+ private final ExportableEntitiesService exportableEntitiesService;
+ private final TbNotificationEntityService entityNotificationService;
+ private final TransactionTemplate transactionTemplate;
+
+ private ListeningExecutorService executor;
+
+ @Value("${vc.thread_pool_size:4}")
+ private int threadPoolSize;
+
+ @PostConstruct
+ public void init() {
+ executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, DefaultEntitiesVersionControlService.class));
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ @Override
+ public ListenableFuture saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
+ var pendingCommit = gitServiceQueue.prepareCommit(user, request);
+
+ return transformAsync(pendingCommit, commit -> {
+ List> gitFutures = new ArrayList<>();
+ switch (request.getType()) {
+ case SINGLE_ENTITY: {
+ handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request));
+ break;
+ }
+ case COMPLEX: {
+ handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request));
+ break;
+ }
+ }
+ return transformAsync(Futures.allAsList(gitFutures), success -> gitServiceQueue.push(commit), executor);
+ }, executor);
+ }
+
+ private void handleSingleEntityRequest(SimpleEntitiesExportCtx ctx) throws Exception {
+ ctx.add(saveEntityData(ctx, ctx.getRequest().getEntityId()));
+ }
+
+ private void handleComplexRequest(ComplexEntitiesExportCtx parentCtx) {
+ ComplexVersionCreateRequest request = parentCtx.getRequest();
+ request.getEntityTypes().forEach((entityType, config) -> {
+ EntityTypeExportCtx ctx = new EntityTypeExportCtx(parentCtx, config, request.getSyncStrategy(), entityType);
+ if (ctx.isOverwrite()) {
+ ctx.add(gitServiceQueue.deleteAll(ctx.getCommit(), entityType));
+ }
+
+ if (config.isAllEntities()) {
+ DaoUtil.processInBatches(pageLink -> exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink)
+ , 100, entity -> {
+ try {
+ ctx.add(saveEntityData(ctx, entity.getId()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } else {
+ for (UUID entityId : config.getEntityIds()) {
+ try {
+ ctx.add(saveEntityData(ctx, EntityIdFactory.getByTypeAndUuid(entityType, entityId)));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ });
+ }
+
+ private ListenableFuture saveEntityData(EntitiesExportCtx> ctx, EntityId entityId) throws Exception {
+ EntityExportData> entityData = exportImportService.exportEntity(ctx, entityId);
+ return gitServiceQueue.addToCommit(ctx.getCommit(), entityData);
+ }
+
+ @Override
+ public ListenableFuture> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, externalId, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listEntityTypeVersions(TenantId tenantId, String branch, EntityType entityType, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, entityType, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listVersions(TenantId tenantId, String branch, PageLink pageLink) throws Exception {
+ return gitServiceQueue.listVersions(tenantId, branch, pageLink);
+ }
+
+ @Override
+ public ListenableFuture> listEntitiesAtVersion(TenantId tenantId, String branch, String versionId, EntityType entityType) throws Exception {
+ return gitServiceQueue.listEntitiesAtVersion(tenantId, branch, versionId, entityType);
+ }
+
+ @Override
+ public ListenableFuture> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception {
+ return gitServiceQueue.listEntitiesAtVersion(tenantId, branch, versionId);
+ }
+
+ @SuppressWarnings({"UnstableApiUsage", "rawtypes"})
+ @Override
+ public ListenableFuture loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception {
+ switch (request.getType()) {
+ case SINGLE_ENTITY: {
+ SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
+ VersionLoadConfig config = versionLoadRequest.getConfig();
+ ListenableFuture future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId());
+ return Futures.transform(future, entityData -> doInTemplate(user, request, ctx -> loadSingleEntity(ctx, config, entityData)), executor);
+ }
+ case ENTITY_TYPE: {
+ EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request;
+ return executor.submit(() -> doInTemplate(user, request, ctx -> loadMultipleEntities(ctx, versionLoadRequest)));
+ }
+ default:
+ throw new IllegalArgumentException("Unsupported version load request");
+ }
+ }
+
+ private VersionLoadResult doInTemplate(SecurityUser user, VersionLoadRequest request, Function function) {
+ try {
+ EntitiesImportCtx ctx = new EntitiesImportCtx(user, request.getVersionId());
+ VersionLoadResult result = transactionTemplate.execute(status -> function.apply(ctx));
+ try {
+ for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) {
+ throwingRunnable.run();
+ }
+ } catch (ThingsboardException e) {
+ throw new RuntimeException(e);
+ }
+ return result;
+ } catch (LoadEntityException e) {
+ return onError(e.getData(), e.getCause());
+ } catch (Exception e) {
+ log.info("[{}] Failed to process request [{}] due to: ", user.getTenantId(), request, e);
+ throw e;
+ }
+ }
+
+ private VersionLoadResult loadSingleEntity(EntitiesImportCtx ctx, VersionLoadConfig config, EntityExportData entityData) {
+ try {
+ ctx.setSettings(EntityImportSettings.builder()
+ .updateRelations(config.isLoadRelations())
+ .saveAttributes(config.isLoadAttributes())
+ .saveCredentials(config.isLoadCredentials())
+ .findExistingByName(false)
+ .build());
+ ctx.setFinalImportAttempt(true);
+ EntityImportResult> importResult = exportImportService.importEntity(ctx, entityData);
+
+ exportImportService.saveReferencesAndRelations(ctx);
+
+ return VersionLoadResult.success(EntityTypeLoadResult.builder()
+ .entityType(importResult.getEntityType())
+ .created(importResult.getOldEntity() == null ? 1 : 0)
+ .updated(importResult.getOldEntity() != null ? 1 : 0)
+ .deleted(0)
+ .build());
+ } catch (Exception e) {
+ throw new LoadEntityException(entityData, e);
+ }
+ }
+
+ @SneakyThrows
+ private VersionLoadResult loadMultipleEntities(EntitiesImportCtx ctx, EntityTypeVersionLoadRequest request) {
+ var sw = TbStopWatch.create("before");
+
+ List entityTypes = request.getEntityTypes().keySet().stream()
+ .sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
+ for (EntityType entityType : entityTypes) {
+ log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
+ sw.startNew("Entities " + entityType.name());
+ ctx.setSettings(getEntityImportSettings(request, entityType));
+ importEntities(ctx, entityType);
+ }
+
+ sw.startNew("Reimport");
+ reimport(ctx);
+
+ sw.startNew("Remove Others");
+ request.getEntityTypes().keySet().stream()
+ .filter(entityType -> request.getEntityTypes().get(entityType).isRemoveOtherEntities())
+ .sorted(exportImportService.getEntityTypeComparatorForImport().reversed())
+ .forEach(entityType -> removeOtherEntities(ctx, entityType));
+
+ sw.startNew("References and Relations");
+
+ exportImportService.saveReferencesAndRelations(ctx);
+
+ sw.stop();
+ for (var task : sw.getTaskInfo()) {
+ log.info("[{}] Executed: {} in {}ms", ctx.getTenantId(), task.getTaskName(), task.getTimeMillis());
+ }
+ log.info("[{}] Total time: {}ms", ctx.getTenantId(), sw.getTotalTimeMillis());
+ return VersionLoadResult.success(new ArrayList<>(ctx.getResults().values()));
+ }
+
+ private EntityImportSettings getEntityImportSettings(EntityTypeVersionLoadRequest request, EntityType entityType) {
+ var config = request.getEntityTypes().get(entityType);
+ return EntityImportSettings.builder()
+ .updateRelations(config.isLoadRelations())
+ .saveAttributes(config.isLoadAttributes())
+ .findExistingByName(config.isFindExistingEntityByName())
+ .build();
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void importEntities(EntitiesImportCtx ctx, EntityType entityType) {
+ int limit = 100;
+ int offset = 0;
+ List entityDataList;
+ do {
+ try {
+ entityDataList = gitServiceQueue.getEntities(ctx.getTenantId(), ctx.getVersionId(), entityType, offset, limit).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ for (EntityExportData entityData : entityDataList) {
+ EntityExportData reimportBackup = JacksonUtil.clone(entityData);
+ log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
+ EntityImportResult> importResult;
+ try {
+ importResult = exportImportService.importEntity(ctx, entityData);
+ } catch (Exception e) {
+ throw new LoadEntityException(entityData, e);
+ }
+ if (!importResult.isUpdatedAllExternalIds()) {
+ ctx.getToReimport().put(entityData.getEntity().getExternalId(), new ReimportTask(reimportBackup, ctx.getSettings()));
+ continue;
+ }
+
+ registerResult(ctx, entityType, importResult);
+ ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>())
+ .add(importResult.getSavedEntity().getId());
+ }
+ offset += limit;
+ } while (entityDataList.size() == limit);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void reimport(EntitiesImportCtx ctx) {
+ ctx.setFinalImportAttempt(true);
+ ctx.getToReimport().forEach((externalId, task) -> {
+ try {
+ EntityExportData entityData = task.getData();
+ var settings = task.getSettings();
+ ctx.setSettings(settings);
+ EntityImportResult> importResult = exportImportService.importEntity(ctx, entityData);
+
+ registerResult(ctx, externalId.getEntityType(), importResult);
+ ctx.getImportedEntities().computeIfAbsent(externalId.getEntityType(), t -> new HashSet<>())
+ .add(importResult.getSavedEntity().getId());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private void removeOtherEntities(EntitiesImportCtx ctx, EntityType entityType) {
+ DaoUtil.processInBatches(pageLink -> {
+ return exportableEntitiesService.findEntitiesByTenantId(ctx.getTenantId(), entityType, pageLink);
+ }, 100, entity -> {
+ if (ctx.getImportedEntities().get(entityType) == null || !ctx.getImportedEntities().get(entityType).contains(entity.getId())) {
+ exportableEntitiesService.removeById(ctx.getTenantId(), entity.getId());
+
+ ctx.addEventCallback(() -> {
+ entityNotificationService.notifyDeleteEntity(ctx.getTenantId(), entity.getId(),
+ entity, null, ActionType.DELETED, null, ctx.getUser());
+ });
+ ctx.registerDeleted(entityType);
+ }
+ });
+ }
+
+ private VersionLoadResult onError(EntityExportData> entityData, Throwable e) {
+ return analyze(e, entityData).orElseThrow(() -> new RuntimeException(e));
+ }
+
+ private Optional analyze(Throwable e, EntityExportData> entityData) {
+ if (e == null) {
+ return Optional.empty();
+ } else {
+ if (e instanceof DeviceCredentialsValidationException) {
+ return Optional.of(VersionLoadResult.error(EntityLoadError.credentialsError(entityData.getExternalId())));
+ } else if (e instanceof MissingEntityException) {
+ return Optional.of(VersionLoadResult.error(EntityLoadError.referenceEntityError(entityData.getExternalId(), ((MissingEntityException) e).getEntityId())));
+ } else {
+ return analyze(e.getCause(), entityData);
+ }
+ }
+ }
+
+ @Override
+ public ListenableFuture compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception {
+ HasId entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId);
+ if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type");
+
+ EntityId externalId = ((ExportableEntity) entity).getExternalId();
+ if (externalId == null) externalId = entityId;
+
+ return transformAsync(gitServiceQueue.getEntity(user.getTenantId(), versionId, externalId),
+ otherVersion -> {
+ SimpleEntitiesExportCtx ctx = new SimpleEntitiesExportCtx(user, null, null, EntityExportSettings.builder()
+ .exportRelations(otherVersion.hasRelations())
+ .exportAttributes(otherVersion.hasAttributes())
+ .exportCredentials(otherVersion.hasCredentials())
+ .build());
+ EntityExportData> currentVersion = exportImportService.exportEntity(ctx, entityId);
+ return transform(gitServiceQueue.getContentsDiff(user.getTenantId(),
+ JacksonUtil.toPrettyString(currentVersion.sort()),
+ JacksonUtil.toPrettyString(otherVersion.sort())),
+ rawDiff -> new EntityDataDiff(currentVersion, otherVersion, rawDiff), MoreExecutors.directExecutor());
+ }, MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public ListenableFuture getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId) {
+ return Futures.transform(gitServiceQueue.getEntity(user.getTenantId(), versionId, entityId),
+ entity -> new EntityDataInfo(entity.hasRelations(), entity.hasAttributes(), entity.hasCredentials()), MoreExecutors.directExecutor());
+ }
+
+
+ @Override
+ public ListenableFuture> listBranches(TenantId tenantId) throws Exception {
+ return gitServiceQueue.listBranches(tenantId);
+ }
+
+ @Override
+ public RepositorySettings getVersionControlSettings(TenantId tenantId) {
+ return repositorySettingsService.get(tenantId);
+ }
+
+ @Override
+ public ListenableFuture saveVersionControlSettings(TenantId tenantId, RepositorySettings versionControlSettings) {
+ var restoredSettings = this.repositorySettingsService.restore(tenantId, versionControlSettings);
+ try {
+ var future = gitServiceQueue.initRepository(tenantId, restoredSettings);
+ return Futures.transform(future, f -> repositorySettingsService.save(tenantId, restoredSettings), MoreExecutors.directExecutor());
+ } catch (Exception e) {
+ log.debug("{} Failed to init repository: {}", tenantId, versionControlSettings, e);
+ throw new RuntimeException("Failed to init repository!", e);
+ }
+ }
+
+ @Override
+ public ListenableFuture deleteVersionControlSettings(TenantId tenantId) throws Exception {
+ if (repositorySettingsService.delete(tenantId)) {
+ return gitServiceQueue.clearRepository(tenantId);
+ } else {
+ return Futures.immediateFuture(null);
+ }
+ }
+
+ @Override
+ public ListenableFuture checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws ThingsboardException {
+ settings = this.repositorySettingsService.restore(tenantId, settings);
+ try {
+ return gitServiceQueue.testRepository(tenantId, settings);
+ } catch (Exception e) {
+ throw new ThingsboardException(String.format("Unable to access repository: %s", getCauseMessage(e)),
+ ThingsboardErrorCode.GENERAL);
+ }
+ }
+
+ @Override
+ public ListenableFuture autoCommit(SecurityUser user, EntityId entityId) throws Exception {
+ var repositorySettings = repositorySettingsService.get(user.getTenantId());
+ if (repositorySettings == null) {
+ return Futures.immediateFuture(null);
+ }
+ var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
+ if (autoCommitSettings == null) {
+ return Futures.immediateFuture(null);
+ }
+ var entityType = entityId.getEntityType();
+ AutoVersionCreateConfig autoCommitConfig = autoCommitSettings.get(entityType);
+ if (autoCommitConfig == null) {
+ return Futures.immediateFuture(null);
+ }
+ SingleEntityVersionCreateRequest vcr = new SingleEntityVersionCreateRequest();
+ var autoCommitBranchName = autoCommitConfig.getBranch();
+ if (StringUtils.isEmpty(autoCommitBranchName)) {
+ autoCommitBranchName = StringUtils.isNotEmpty(repositorySettings.getDefaultBranch()) ? repositorySettings.getDefaultBranch() : "auto-commits";
+ }
+ vcr.setBranch(autoCommitBranchName);
+ vcr.setVersionName("auto-commit at " + Instant.ofEpochSecond(System.currentTimeMillis() / 1000));
+ vcr.setEntityId(entityId);
+ vcr.setConfig(autoCommitConfig);
+ return saveEntitiesVersion(user, vcr);
+ }
+
+ @Override
+ public ListenableFuture autoCommit(SecurityUser user, EntityType entityType, List entityIds) throws Exception {
+ var repositorySettings = repositorySettingsService.get(user.getTenantId());
+ if (repositorySettings == null) {
+ return Futures.immediateFuture(null);
+ }
+ var autoCommitSettings = autoCommitSettingsService.get(user.getTenantId());
+ if (autoCommitSettings == null) {
+ return Futures.immediateFuture(null);
+ }
+ AutoVersionCreateConfig autoCommitConfig = autoCommitSettings.get(entityType);
+ if (autoCommitConfig == null) {
+ return Futures.immediateFuture(null);
+ }
+ var autoCommitBranchName = autoCommitConfig.getBranch();
+ if (StringUtils.isEmpty(autoCommitBranchName)) {
+ autoCommitBranchName = StringUtils.isNotEmpty(repositorySettings.getDefaultBranch()) ? repositorySettings.getDefaultBranch() : "auto-commits";
+ }
+ ComplexVersionCreateRequest vcr = new ComplexVersionCreateRequest();
+ vcr.setBranch(autoCommitBranchName);
+ vcr.setVersionName("auto-commit at " + Instant.ofEpochSecond(System.currentTimeMillis() / 1000));
+ vcr.setSyncStrategy(SyncStrategy.MERGE);
+
+ EntityTypeVersionCreateConfig vcrConfig = new EntityTypeVersionCreateConfig();
+ vcrConfig.setEntityIds(entityIds);
+ vcr.setEntityTypes(Collections.singletonMap(entityType, vcrConfig));
+ return saveEntitiesVersion(user, vcr);
+ }
+
+ private String getCauseMessage(Exception e) {
+ String message;
+ if (e.getCause() != null && StringUtils.isNotEmpty(e.getCause().getMessage())) {
+ message = e.getCause().getMessage();
+ } else {
+ message = e.getMessage();
+ }
+ return message;
+ }
+
+ private void registerResult(EntitiesImportCtx ctx, EntityType entityType, EntityImportResult> importResult) {
+ if (importResult.isCreated()) {
+ ctx.registerResult(entityType, true);
+ } else if (importResult.isUpdated() || importResult.isUpdatedRelatedEntities()) {
+ ctx.registerResult(entityType, false);
+ }
+ }
+
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
new file mode 100644
index 0000000000..f5585fac39
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitVersionControlQueueService.java
@@ -0,0 +1,526 @@
+/**
+ * Copyright © 2016-2022 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.vc;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.protobuf.ByteString;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.cluster.TbClusterService;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.ExportableEntity;
+import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.sync.ie.EntityExportData;
+import org.thingsboard.server.common.data.sync.vc.EntityVersion;
+import org.thingsboard.server.common.data.sync.vc.EntityVersionsDiff;
+import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
+import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
+import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
+import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
+import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.gen.transport.TransportProtos.CommitRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.EntitiesContentRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.EntityContentRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.GenericRepositoryRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ListEntitiesRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ListVersionsRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.PrepareMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.VersionControlResponseMsg;
+import org.thingsboard.server.queue.TbQueueCallback;
+import org.thingsboard.server.queue.TbQueueMsgMetadata;
+import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
+import org.thingsboard.server.queue.scheduler.SchedulerComponent;
+import org.thingsboard.server.queue.util.DataDecodingEncodingService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.sync.vc.data.ClearRepositoryGitRequest;
+import org.thingsboard.server.service.sync.vc.data.CommitGitRequest;
+import org.thingsboard.server.service.sync.vc.data.ContentsDiffGitRequest;
+import org.thingsboard.server.service.sync.vc.data.EntitiesContentGitRequest;
+import org.thingsboard.server.service.sync.vc.data.EntityContentGitRequest;
+import org.thingsboard.server.service.sync.vc.data.ListBranchesGitRequest;
+import org.thingsboard.server.service.sync.vc.data.ListEntitiesGitRequest;
+import org.thingsboard.server.service.sync.vc.data.ListVersionsGitRequest;
+import org.thingsboard.server.service.sync.vc.data.PendingGitRequest;
+import org.thingsboard.server.service.sync.vc.data.VersionsDiffGitRequest;
+import org.thingsboard.server.service.sync.vc.data.VoidGitRequest;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@TbCoreComponent
+@Service
+@Slf4j
+public class DefaultGitVersionControlQueueService implements GitVersionControlQueueService {
+
+ private final TbServiceInfoProvider serviceInfoProvider;
+ private final TbClusterService clusterService;
+ private final DataDecodingEncodingService encodingService;
+ private final DefaultEntitiesVersionControlService entitiesVersionControlService;
+ private final SchedulerComponent scheduler;
+
+ private final Map> pendingRequestMap = new HashMap<>();
+
+ @Value("${queue.vc.request-timeout:60000}")
+ private int requestTimeout;
+
+ public DefaultGitVersionControlQueueService(TbServiceInfoProvider serviceInfoProvider, TbClusterService clusterService,
+ DataDecodingEncodingService encodingService,
+ @Lazy DefaultEntitiesVersionControlService entitiesVersionControlService,
+ SchedulerComponent scheduler) {
+ this.serviceInfoProvider = serviceInfoProvider;
+ this.clusterService = clusterService;
+ this.encodingService = encodingService;
+ this.entitiesVersionControlService = entitiesVersionControlService;
+ this.scheduler = scheduler;
+ }
+
+ @Override
+ public ListenableFuture prepareCommit(User user, VersionCreateRequest request) {
+ SettableFuture future = SettableFuture.create();
+
+ CommitGitRequest commit = new CommitGitRequest(user.getTenantId(), request);
+ registerAndSend(commit, builder -> builder.setCommitRequest(
+ buildCommitRequest(commit).setPrepareMsg(getCommitPrepareMsg(user, request)).build()
+ ).build(), wrap(future, commit));
+ return future;
+ }
+
+ @Override
+ public ListenableFuture addToCommit(CommitGitRequest commit, EntityExportData> entityData) {
+ SettableFuture