diff --git a/application/pom.xml b/application/pom.xml
index 1b89752925..2bfab2c858 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -337,6 +337,10 @@
Java-WebSocket
test
+
+ org.eclipse.jgit
+ org.eclipse.jgit
+
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
new file mode 100644
index 0000000000..ad307a6576
--- /dev/null
+++ b/application/src/main/data/upgrade/3.3.4/schema_update.sql
@@ -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.
+--
+
+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 dashboard
+ ADD COLUMN IF NOT EXISTS external_id UUID;
+ALTER TABLE customer
+ ADD COLUMN IF NOT EXISTS external_id UUID;
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 7bd9cb065d..66b570cf1d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -54,8 +54,8 @@ 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.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.exportimport.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/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 85a21e912c..4b6eca7879 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -74,8 +74,8 @@ 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.gateway_device.GatewayNotificationsService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.exportimport.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/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
index f3d73f13e5..948a380fcd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
@@ -55,8 +55,8 @@ 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.edge.EdgeBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportRequest;
-import org.thingsboard.server.service.importing.BulkImportResult;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.BulkImportRequest;
+import org.thingsboard.server.service.sync.exportimport.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..e7df5acdc3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
@@ -0,0 +1,336 @@
+/**
+ * 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 io.swagger.annotations.ApiModelProperty;
+import io.swagger.annotations.ApiOperation;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+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.RestController;
+import org.thingsboard.server.common.data.EntityType;
+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.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
+import org.thingsboard.server.service.sync.vc.data.EntitiesVersionControlSettings;
+import org.thingsboard.server.service.sync.vc.data.EntityVersion;
+import org.thingsboard.server.service.sync.vc.data.VersionCreationResult;
+import org.thingsboard.server.service.sync.vc.data.VersionLoadResult;
+import org.thingsboard.server.service.sync.vc.data.VersionedEntityInfo;
+import org.thingsboard.server.service.sync.vc.data.request.create.VersionCreateRequest;
+import org.thingsboard.server.service.sync.vc.data.request.load.VersionLoadRequest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
+
+@RestController
+@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 +
+ "ENTITY_LIST:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"ENTITY_LIST\",\n" +
+ "\n" +
+ " \"versionName\": \"Version 1.0\",\n" +
+ " \"branch\": \"dev\",\n" +
+ "\n" +
+ " \"entitiesIds\": [\n" +
+ " {\n" +
+ " \"entityType\": \"DEVICE\",\n" +
+ " \"id\": \"b79448e0-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"entityType\": \"DEVICE_PROFILE\",\n" +
+ " \"id\": \"b7944123-d4f4-11ec-847b-0f432358ab48\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"config\": {\n" +
+ " \"saveRelations\": true,\n" +
+ " \"syncStrategy\": \"MERGE\"\n" +
+ " }\n" +
+ "}\n```" + NEW_LINE +
+ "ENTITY_TYPE:" + NEW_LINE +
+ "```\n{\n" +
+ " \"type\": \"ENTITY_TYPE\",\n" +
+ "\n" +
+ " \"versionName\": \"Version 1.0\",\n" +
+ " \"branch\": \"dev\",\n" +
+ "\n" +
+ " \"entityTypes\": {\n" +
+ " \"DEVICE\": {\n" +
+ " \"saveRelations\": true,\n" +
+ " \"syncStrategy\": \"MERGE\"\n" +
+ " },\n" +
+ " \"DEVICE_PROFILE\": {\n" +
+ " \"saveRelations\": true,\n" +
+ " \"syncStrategy\": \"OVERWRITE\"\n" +
+ " }\n" +
+ " }\n" +
+ "}\n```")
+ @PostMapping("/version")
+ public VersionCreationResult saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ return 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("/version/{branch}/{entityType}/{externalEntityUuid}")
+ public List listEntityVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable UUID externalEntityUuid) throws ThingsboardException {
+ try {
+ EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
+ return versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n[\n" +
+ " {\n" +
+ " \"id\": \"c30c8bcaed3f0813649f0dee51a89d04d0a12b28\",\n" +
+ " \"name\": \"Device profiles from dev\"\n" +
+ " }\n" +
+ "]\n```")
+ @GetMapping("/version/{branch}/{entityType}")
+ public List listEntityTypeVersions(@PathVariable String branch,
+ @PathVariable EntityType entityType) throws ThingsboardException {
+ try {
+ return versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType);
+ } 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("/version/{branch}")
+ public List listVersions(@PathVariable String branch) throws ThingsboardException {
+ try {
+ return versionControlService.listVersions(getTenantId(), branch);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @GetMapping("/entity/{branch}/{entityType}/{versionId}")
+ public List listEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable EntityType entityType,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ return versionControlService.listEntitiesAtVersion(getTenantId(), entityType, branch, versionId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @GetMapping("/entity/{branch}/{versionId}")
+ public List listAllEntitiesAtVersion(@PathVariable String branch,
+ @PathVariable String versionId) throws ThingsboardException {
+ try {
+ return versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, 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 List loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ try {
+ String versionId = request.getVersionId();
+ if (versionId == null) {
+ List versions = versionControlService.listVersions(user.getTenantId(), request.getBranch());
+ if (versions.size() > 0) {
+ versionId = versions.get(0).getId();
+ } else {
+ throw new IllegalArgumentException("No versions available in branch");
+ }
+ }
+
+ return 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 List listBranches() throws ThingsboardException {
+ try {
+ List remoteBranches = versionControlService.listBranches(getTenantId());
+ List infos = new ArrayList<>();
+
+ String defaultBranch = getSettings().getDefaultBranch();
+ if (StringUtils.isNotEmpty(defaultBranch)) {
+ remoteBranches.remove(defaultBranch);
+ infos.add(new BranchInfo(defaultBranch, true));
+ }
+
+ remoteBranches.forEach(branch -> infos.add(new BranchInfo(branch, false)));
+ return infos;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n{\n" +
+ " \"repositoryUri\": \"https://github.com/User/repo.git\",\n" +
+ " \"username\": \"User\",\n" +
+ " \"password\": \"api_key\",\n" +
+ " \"defaultBranch\": \"master\"\n" +
+ "}\n```")
+ @GetMapping("/settings")
+ public EntitiesVersionControlSettings getSettings() throws ThingsboardException {
+ try {
+ return versionControlService.getSettings(getTenantId());
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @ApiOperation(value = "", notes = "" +
+ "```\n{\n" +
+ " \"repositoryUri\": \"https://github.com/User/repo.git\",\n" +
+ " \"username\": \"User\",\n" +
+ " \"password\": \"api_key\",\n" +
+ " \"defaultBranch\": \"master\"\n" +
+ "}\n```")
+ @PostMapping("/settings")
+ public void saveSettings(@RequestBody EntitiesVersionControlSettings settings) throws ThingsboardException {
+ try {
+ versionControlService.saveSettings(getTenantId(), settings);
+ } 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/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/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
index 4c75f7d45d..179c9c697d 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
@@ -25,8 +25,8 @@ 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.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.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 cd655a2467..3e63652b97 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,8 +49,8 @@ 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.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.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 615a7cddd2..75f7133721 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
@@ -25,8 +25,8 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.service.importing.AbstractBulkImportService;
-import org.thingsboard.server.service.importing.BulkImportColumnType;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.AbstractBulkImportService;
+import org.thingsboard.server.service.sync.exportimport.importing.csv.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/exportimport/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/DefaultEntitiesExportImportService.java
new file mode 100644
index 0000000000..108c2c0b1b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/DefaultEntitiesExportImportService.java
@@ -0,0 +1,174 @@
+/**
+ * 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.exportimport;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+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.dao.exception.DataValidationException;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.exportimport.exporting.EntityExportService;
+import org.thingsboard.server.service.sync.exportimport.exporting.data.EntityExportData;
+import org.thingsboard.server.service.sync.exportimport.exporting.data.EntityExportSettings;
+import org.thingsboard.server.service.sync.exportimport.exporting.impl.BaseEntityExportService;
+import org.thingsboard.server.service.sync.exportimport.exporting.impl.DefaultEntityExportService;
+import org.thingsboard.server.service.sync.exportimport.importing.EntityImportService;
+import org.thingsboard.server.service.sync.exportimport.importing.data.EntityImportResult;
+import org.thingsboard.server.service.sync.exportimport.importing.data.EntityImportSettings;
+import org.thingsboard.server.utils.ThrowingRunnable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultEntitiesExportImportService implements EntitiesExportImportService {
+
+ private final Map> exportServices = new HashMap<>();
+ private final Map> importServices = new HashMap<>();
+
+ protected static final List SUPPORTED_ENTITY_TYPES = List.of(
+ EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN,
+ EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE
+ );
+
+
+ @Override
+ public , I extends EntityId> EntityExportData exportEntity(SecurityUser user, I entityId, EntityExportSettings exportSettings) throws ThingsboardException {
+ EntityType entityType = entityId.getEntityType();
+ EntityExportService> exportService = getExportService(entityType);
+
+ return exportService.getExportData(user, entityId, exportSettings);
+ }
+
+
+ @Override
+ public , I extends EntityId> EntityImportResult importEntity(SecurityUser user, EntityExportData exportData, EntityImportSettings importSettings,
+ boolean saveReferences, boolean sendEvents) throws ThingsboardException {
+ 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(user, exportData, importSettings);
+
+ if (saveReferences) {
+ importResult.getSaveReferencesCallback().run();
+ }
+ if (sendEvents) {
+ importResult.getSendEventsCallback().run();
+ }
+
+ return importResult;
+ }
+
+ @Transactional(rollbackFor = Exception.class, timeout = 120)
+ @Override
+ public List> importEntities(SecurityUser user, List> exportDataList, EntityImportSettings importSettings) throws ThingsboardException {
+ fixDataOrderForImport(exportDataList);
+
+ List> importResults = new ArrayList<>();
+
+ for (EntityExportData exportData : exportDataList) {
+ EntityImportResult> importResult = importEntity(user, exportData, importSettings, false, false);
+ importResults.add(importResult);
+ }
+
+ for (ThrowingRunnable saveReferencesCallback : importResults.stream()
+ .map(EntityImportResult::getSaveReferencesCallback)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList())) {
+ saveReferencesCallback.run();
+ }
+
+ importResults.stream()
+ .map(EntityImportResult::getSendEventsCallback)
+ .filter(Objects::nonNull)
+ .forEach(sendEventsCallback -> {
+ try {
+ sendEventsCallback.run();
+ } catch (Exception e) {
+ log.error("Failed to send event for entity", e);
+ }
+ });
+
+ return importResults;
+ }
+
+ @Override
+ public void fixDataOrderForImport(List> exportDataList) {
+ exportDataList.sort(Comparator.comparing(exportData -> SUPPORTED_ENTITY_TYPES.indexOf(exportData.getEntityType())));
+ }
+
+
+ @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/exportimport/EntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/EntitiesExportImportService.java
new file mode 100644
index 0000000000..049cf4390a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/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.exportimport;
+
+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.service.security.model.SecurityUser;
+import org.thingsboard.server.service.sync.exportimport.exporting.data.EntityExportData;
+import org.thingsboard.server.service.sync.exportimport.exporting.data.EntityExportSettings;
+import org.thingsboard.server.service.sync.exportimport.importing.data.EntityImportResult;
+import org.thingsboard.server.service.sync.exportimport.importing.data.EntityImportSettings;
+
+import java.util.List;
+
+public interface EntitiesExportImportService {
+
+ , I extends EntityId> EntityExportData exportEntity(SecurityUser user, I entityId, EntityExportSettings exportSettings) throws ThingsboardException;
+
+ , I extends EntityId> EntityImportResult importEntity(SecurityUser user, EntityExportData exportData, EntityImportSettings importSettings,
+ boolean saveReferences, boolean sendEvents) throws ThingsboardException;
+
+ List> importEntities(SecurityUser user, List> exportDataList, EntityImportSettings importSettings) throws ThingsboardException;
+
+ void fixDataOrderForImport(List> exportDataList);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/sync/exportimport/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/exporting/DefaultExportableEntitiesService.java
new file mode 100644
index 0000000000..dafd68ba7b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sync/exportimport/exporting/DefaultExportableEntitiesService.java
@@ -0,0 +1,204 @@
+/**
+ * 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.exportimport.exporting;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+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.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.CustomerId;
+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;
+import org.thingsboard.server.common.data.query.EntityData;
+import org.thingsboard.server.common.data.query.EntityDataPageLink;
+import org.thingsboard.server.common.data.query.EntityDataQuery;
+import org.thingsboard.server.common.data.query.EntityDataSortOrder;
+import org.thingsboard.server.common.data.query.EntityFilter;
+import org.thingsboard.server.common.data.query.EntityKey;
+import org.thingsboard.server.common.data.query.EntityKeyType;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.ExportableEntityDao;
+import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.model.SecurityUser;
+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 java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.dao.sql.query.EntityKeyMapping.CREATED_TIME;
+
+@Service
+@TbCoreComponent
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultExportableEntitiesService implements ExportableEntitiesService {
+
+ private final Map> daos = new HashMap<>();
+
+ private final EntityService entityService;
+ 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) {
+ EntityType entityType = id.getEntityType();
+ Dao dao = getDao(entityType);
+ if (dao == null) {
+ throw new IllegalArgumentException("Unsupported entity type " + entityType);
+ }
+
+ E entity = dao.findById(tenantId, id.getId());
+
+ if (entity == null || !belongsToTenant(entity, tenantId)) {
+ return null;
+ }
+
+ return entity;
+ }
+
+ @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) {
+ Dao dao = getDao(entityType);
+ if (dao instanceof ExportableEntityDao) {
+ ExportableEntityDao exportableEntityDao = (ExportableEntityDao) dao;
+ return exportableEntityDao.findByTenantId(tenantId.getId(), pageLink);
+ }
+ return new PageData<>();
+ }
+
+ private boolean belongsToTenant(HasId extends EntityId> entity, TenantId tenantId) {
+ return tenantId.equals(((HasTenantId) entity).getTenantId());
+ }
+
+
+ private List findEntitiesByFilter(TenantId tenantId, CustomerId customerId, EntityFilter filter, int page, int pageSize) {
+ EntityDataPageLink pageLink = new EntityDataPageLink();
+ pageLink.setPage(page);
+ pageLink.setPageSize(pageSize);
+ EntityKey sortProperty = new EntityKey(EntityKeyType.ENTITY_FIELD, CREATED_TIME);
+ pageLink.setSortOrder(new EntityDataSortOrder(sortProperty, EntityDataSortOrder.Direction.DESC));
+
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, List.of(sortProperty), Collections.emptyList(), Collections.emptyList());
+ return findEntitiesByQuery(tenantId, customerId, query);
+ }
+
+ private List findEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query) {
+ try {
+ return entityService.findEntityDataByQuery(tenantId, customerId, query).getData().stream()
+ .map(EntityData::getEntityId)
+ .collect(Collectors.toList());
+ } catch (DataAccessException e) {
+ log.error("Failed to find entity data by query: {}", e.getMessage());
+ throw new IllegalArgumentException("Entity filter cannot be processed");
+ }
+ }
+
+ @Override
+ public void deleteByTenantIdAndId(TenantId tenantId, I id) {
+ EntityType entityType = id.getEntityType();
+ Dao