Commit 8518b993a331117862622811f3246d701d3587fa

Authored by Igor Kulikov
Committed by GitHub
2 parents ad6a52cf ffe16b8d

Merge pull request #5220 from thingsboard/feature/bulk-import/device-credentials

[3.3.2] Feature/bulk import/device credentials
Showing 51 changed files with 2006 additions and 237 deletions
@@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import lombok.RequiredArgsConstructor;
  20 +import lombok.extern.slf4j.Slf4j;
19 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.PathVariable; 23 import org.springframework.web.bind.annotation.PathVariable;
  24 +import org.springframework.web.bind.annotation.PostMapping;
22 import org.springframework.web.bind.annotation.RequestBody; 25 import org.springframework.web.bind.annotation.RequestBody;
23 import org.springframework.web.bind.annotation.RequestMapping; 26 import org.springframework.web.bind.annotation.RequestMapping;
24 import org.springframework.web.bind.annotation.RequestMethod; 27 import org.springframework.web.bind.annotation.RequestMethod;
@@ -34,7 +37,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo; @@ -34,7 +37,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
34 import org.thingsboard.server.common.data.asset.AssetSearchQuery; 37 import org.thingsboard.server.common.data.asset.AssetSearchQuery;
35 import org.thingsboard.server.common.data.audit.ActionType; 38 import org.thingsboard.server.common.data.audit.ActionType;
36 import org.thingsboard.server.common.data.edge.Edge; 39 import org.thingsboard.server.common.data.edge.Edge;
37 -import org.thingsboard.server.common.data.edge.EdgeEventType;  
38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 40 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 import org.thingsboard.server.common.data.edge.EdgeEventActionType; 41 import org.thingsboard.server.common.data.edge.EdgeEventActionType;
40 import org.thingsboard.server.common.data.exception.ThingsboardException; 42 import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.page.TimePageLink; @@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.page.TimePageLink;
48 import org.thingsboard.server.dao.exception.IncorrectParameterException; 50 import org.thingsboard.server.dao.exception.IncorrectParameterException;
49 import org.thingsboard.server.dao.model.ModelConstants; 51 import org.thingsboard.server.dao.model.ModelConstants;
50 import org.thingsboard.server.queue.util.TbCoreComponent; 52 import org.thingsboard.server.queue.util.TbCoreComponent;
  53 +import org.thingsboard.server.service.asset.AssetBulkImportService;
  54 +import org.thingsboard.server.service.importing.BulkImportRequest;
  55 +import org.thingsboard.server.service.importing.BulkImportResult;
51 import org.thingsboard.server.service.security.model.SecurityUser; 56 import org.thingsboard.server.service.security.model.SecurityUser;
52 import org.thingsboard.server.service.security.permission.Operation; 57 import org.thingsboard.server.service.security.permission.Operation;
53 import org.thingsboard.server.service.security.permission.Resource; 58 import org.thingsboard.server.service.security.permission.Resource;
@@ -63,7 +68,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -63,7 +68,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
63 @RestController 68 @RestController
64 @TbCoreComponent 69 @TbCoreComponent
65 @RequestMapping("/api") 70 @RequestMapping("/api")
  71 +@RequiredArgsConstructor
  72 +@Slf4j
66 public class AssetController extends BaseController { 73 public class AssetController extends BaseController {
  74 + private final AssetBulkImportService assetBulkImportService;
67 75
68 public static final String ASSET_ID = "assetId"; 76 public static final String ASSET_ID = "assetId";
69 77
@@ -108,13 +116,7 @@ public class AssetController extends BaseController { @@ -108,13 +116,7 @@ public class AssetController extends BaseController {
108 116
109 Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); 117 Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
110 118
111 - logEntityAction(savedAsset.getId(), savedAsset,  
112 - savedAsset.getCustomerId(),  
113 - asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);  
114 -  
115 - if (asset.getId() != null) {  
116 - sendEntityNotificationMsg(savedAsset.getTenantId(), savedAsset.getId(), EdgeEventActionType.UPDATED);  
117 - } 119 + onAssetCreatedOrUpdated(savedAsset, asset.getId() != null);
118 120
119 return savedAsset; 121 return savedAsset;
120 } catch (Exception e) { 122 } catch (Exception e) {
@@ -124,6 +126,20 @@ public class AssetController extends BaseController { @@ -124,6 +126,20 @@ public class AssetController extends BaseController {
124 } 126 }
125 } 127 }
126 128
  129 + private void onAssetCreatedOrUpdated(Asset asset, boolean updated) {
  130 + try {
  131 + logEntityAction(asset.getId(), asset,
  132 + asset.getCustomerId(),
  133 + updated ? ActionType.UPDATED : ActionType.ADDED, null);
  134 + } catch (ThingsboardException e) {
  135 + log.error("Failed to log entity action", e);
  136 + }
  137 +
  138 + if (updated) {
  139 + sendEntityNotificationMsg(asset.getTenantId(), asset.getId(), EdgeEventActionType.UPDATED);
  140 + }
  141 + }
  142 +
127 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 143 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
128 @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE) 144 @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
129 @ResponseStatus(value = HttpStatus.OK) 145 @ResponseStatus(value = HttpStatus.OK)
@@ -258,7 +274,7 @@ public class AssetController extends BaseController { @@ -258,7 +274,7 @@ public class AssetController extends BaseController {
258 try { 274 try {
259 TenantId tenantId = getCurrentUser().getTenantId(); 275 TenantId tenantId = getCurrentUser().getTenantId();
260 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); 276 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
261 - if (type != null && type.trim().length()>0) { 277 + if (type != null && type.trim().length() > 0) {
262 return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink)); 278 return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
263 } else { 279 } else {
264 return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink)); 280 return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
@@ -321,7 +337,7 @@ public class AssetController extends BaseController { @@ -321,7 +337,7 @@ public class AssetController extends BaseController {
321 CustomerId customerId = new CustomerId(toUUID(strCustomerId)); 337 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
322 checkCustomerId(customerId, Operation.READ); 338 checkCustomerId(customerId, Operation.READ);
323 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); 339 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
324 - if (type != null && type.trim().length()>0) { 340 + if (type != null && type.trim().length() > 0) {
325 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); 341 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
326 } else { 342 } else {
327 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); 343 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
@@ -426,7 +442,7 @@ public class AssetController extends BaseController { @@ -426,7 +442,7 @@ public class AssetController extends BaseController {
426 @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST) 442 @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST)
427 @ResponseBody 443 @ResponseBody
428 public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId, 444 public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId,
429 - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { 445 + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
430 checkParameter(EDGE_ID, strEdgeId); 446 checkParameter(EDGE_ID, strEdgeId);
431 checkParameter(ASSET_ID, strAssetId); 447 checkParameter(ASSET_ID, strAssetId);
432 try { 448 try {
@@ -444,7 +460,7 @@ public class AssetController extends BaseController { @@ -444,7 +460,7 @@ public class AssetController extends BaseController {
444 460
445 sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE); 461 sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE);
446 462
447 - return savedAsset; 463 + return savedAsset;
448 } catch (Exception e) { 464 } catch (Exception e) {
449 465
450 logEntityAction(emptyId(EntityType.ASSET), null, 466 logEntityAction(emptyId(EntityType.ASSET), null,
@@ -530,4 +546,13 @@ public class AssetController extends BaseController { @@ -530,4 +546,13 @@ public class AssetController extends BaseController {
530 throw handleException(e); 546 throw handleException(e);
531 } 547 }
532 } 548 }
  549 +
  550 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
  551 + @PostMapping("/asset/bulk_import")
  552 + public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
  553 + return assetBulkImportService.processBulkImport(request, getCurrentUser(), importedAssetInfo -> {
  554 + onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated());
  555 + });
  556 + }
  557 +
533 } 558 }
@@ -121,7 +121,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; @@ -121,7 +121,7 @@ import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
121 import org.thingsboard.server.queue.discovery.PartitionService; 121 import org.thingsboard.server.queue.discovery.PartitionService;
122 import org.thingsboard.server.queue.provider.TbQueueProducerProvider; 122 import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
123 import org.thingsboard.server.queue.util.TbCoreComponent; 123 import org.thingsboard.server.queue.util.TbCoreComponent;
124 -import org.thingsboard.server.service.action.RuleEngineEntityActionService; 124 +import org.thingsboard.server.service.action.EntityActionService;
125 import org.thingsboard.server.service.component.ComponentDiscoveryService; 125 import org.thingsboard.server.service.component.ComponentDiscoveryService;
126 import org.thingsboard.server.service.edge.EdgeLicenseService; 126 import org.thingsboard.server.service.edge.EdgeLicenseService;
127 import org.thingsboard.server.service.edge.EdgeNotificationService; 127 import org.thingsboard.server.service.edge.EdgeNotificationService;
@@ -279,7 +279,7 @@ public abstract class BaseController { @@ -279,7 +279,7 @@ public abstract class BaseController {
279 protected EdgeLicenseService edgeLicenseService; 279 protected EdgeLicenseService edgeLicenseService;
280 280
281 @Autowired 281 @Autowired
282 - protected RuleEngineEntityActionService ruleEngineEntityActionService; 282 + protected EntityActionService entityActionService;
283 283
284 @Value("${server.log_controller_error_stack_trace}") 284 @Value("${server.log_controller_error_stack_trace}")
285 @Getter 285 @Getter
@@ -819,13 +819,7 @@ public abstract class BaseController { @@ -819,13 +819,7 @@ public abstract class BaseController {
819 819
820 protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId, 820 protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
821 ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException { 821 ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
822 - if (customerId == null || customerId.isNullUid()) {  
823 - customerId = user.getCustomerId();  
824 - }  
825 - if (e == null) {  
826 - ruleEngineEntityActionService.pushEntityActionToRuleEngine(entityId, entity, user.getTenantId(), customerId, actionType, user, additionalInfo);  
827 - }  
828 - auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo); 822 + entityActionService.logEntityAction(user, entityId, entity, customerId, actionType, e, additionalInfo);
829 } 823 }
830 824
831 825
@@ -19,10 +19,13 @@ import com.google.common.util.concurrent.FutureCallback; @@ -19,10 +19,13 @@ import com.google.common.util.concurrent.FutureCallback;
19 import com.google.common.util.concurrent.Futures; 19 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 import com.google.common.util.concurrent.MoreExecutors; 21 import com.google.common.util.concurrent.MoreExecutors;
  22 +import lombok.RequiredArgsConstructor;
  23 +import lombok.extern.slf4j.Slf4j;
22 import org.springframework.http.HttpStatus; 24 import org.springframework.http.HttpStatus;
23 import org.springframework.http.ResponseEntity; 25 import org.springframework.http.ResponseEntity;
24 import org.springframework.security.access.prepost.PreAuthorize; 26 import org.springframework.security.access.prepost.PreAuthorize;
25 import org.springframework.web.bind.annotation.PathVariable; 27 import org.springframework.web.bind.annotation.PathVariable;
  28 +import org.springframework.web.bind.annotation.PostMapping;
26 import org.springframework.web.bind.annotation.RequestBody; 29 import org.springframework.web.bind.annotation.RequestBody;
27 import org.springframework.web.bind.annotation.RequestMapping; 30 import org.springframework.web.bind.annotation.RequestMapping;
28 import org.springframework.web.bind.annotation.RequestMethod; 31 import org.springframework.web.bind.annotation.RequestMethod;
@@ -56,7 +59,6 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -56,7 +59,6 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
56 import org.thingsboard.server.common.data.page.PageData; 59 import org.thingsboard.server.common.data.page.PageData;
57 import org.thingsboard.server.common.data.page.PageLink; 60 import org.thingsboard.server.common.data.page.PageLink;
58 import org.thingsboard.server.common.data.page.TimePageLink; 61 import org.thingsboard.server.common.data.page.TimePageLink;
59 -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;  
60 import org.thingsboard.server.common.data.security.DeviceCredentials; 62 import org.thingsboard.server.common.data.security.DeviceCredentials;
61 import org.thingsboard.server.common.msg.TbMsg; 63 import org.thingsboard.server.common.msg.TbMsg;
62 import org.thingsboard.server.common.msg.TbMsgDataType; 64 import org.thingsboard.server.common.msg.TbMsgDataType;
@@ -67,6 +69,9 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult; @@ -67,6 +69,9 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult;
67 import org.thingsboard.server.dao.exception.IncorrectParameterException; 69 import org.thingsboard.server.dao.exception.IncorrectParameterException;
68 import org.thingsboard.server.dao.model.ModelConstants; 70 import org.thingsboard.server.dao.model.ModelConstants;
69 import org.thingsboard.server.queue.util.TbCoreComponent; 71 import org.thingsboard.server.queue.util.TbCoreComponent;
  72 +import org.thingsboard.server.service.device.DeviceBulkImportService;
  73 +import org.thingsboard.server.service.importing.BulkImportRequest;
  74 +import org.thingsboard.server.service.importing.BulkImportResult;
70 import org.thingsboard.server.service.security.model.SecurityUser; 75 import org.thingsboard.server.service.security.model.SecurityUser;
71 import org.thingsboard.server.service.security.permission.Operation; 76 import org.thingsboard.server.service.security.permission.Operation;
72 import org.thingsboard.server.service.security.permission.Resource; 77 import org.thingsboard.server.service.security.permission.Resource;
@@ -83,7 +88,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -83,7 +88,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
83 @RestController 88 @RestController
84 @TbCoreComponent 89 @TbCoreComponent
85 @RequestMapping("/api") 90 @RequestMapping("/api")
  91 +@RequiredArgsConstructor
  92 +@Slf4j
86 public class DeviceController extends BaseController { 93 public class DeviceController extends BaseController {
  94 + private final DeviceBulkImportService deviceBulkImportService;
87 95
88 private static final String DEVICE_ID = "deviceId"; 96 private static final String DEVICE_ID = "deviceId";
89 private static final String DEVICE_NAME = "deviceName"; 97 private static final String DEVICE_NAME = "deviceName";
@@ -133,11 +141,7 @@ public class DeviceController extends BaseController { @@ -133,11 +141,7 @@ public class DeviceController extends BaseController {
133 141
134 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); 142 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
135 143
136 - tbClusterService.onDeviceUpdated(savedDevice, oldDevice);  
137 -  
138 - logEntityAction(savedDevice.getId(), savedDevice,  
139 - savedDevice.getCustomerId(),  
140 - created ? ActionType.ADDED : ActionType.UPDATED, null); 144 + onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created);
141 145
142 return savedDevice; 146 return savedDevice;
143 } catch (Exception e) { 147 } catch (Exception e) {
@@ -148,6 +152,18 @@ public class DeviceController extends BaseController { @@ -148,6 +152,18 @@ public class DeviceController extends BaseController {
148 152
149 } 153 }
150 154
  155 + private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated) {
  156 + tbClusterService.onDeviceUpdated(savedDevice, oldDevice);
  157 +
  158 + try {
  159 + logEntityAction(savedDevice.getId(), savedDevice,
  160 + savedDevice.getCustomerId(),
  161 + updated ? ActionType.UPDATED : ActionType.ADDED, null);
  162 + } catch (ThingsboardException e) {
  163 + log.error("Failed to log entity action", e);
  164 + }
  165 + }
  166 +
151 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 167 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
152 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE) 168 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE)
153 @ResponseStatus(value = HttpStatus.OK) 169 @ResponseStatus(value = HttpStatus.OK)
@@ -776,4 +792,13 @@ public class DeviceController extends BaseController { @@ -776,4 +792,13 @@ public class DeviceController extends BaseController {
776 throw handleException(e); 792 throw handleException(e);
777 } 793 }
778 } 794 }
  795 +
  796 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
  797 + @PostMapping("/device/bulk_import")
  798 + public BulkImportResult<Device> processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception {
  799 + return deviceBulkImportService.processBulkImport(request, getCurrentUser(), importedDeviceInfo -> {
  800 + onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated());
  801 + });
  802 + }
  803 +
779 } 804 }
@@ -17,11 +17,13 @@ package org.thingsboard.server.controller; @@ -17,11 +17,13 @@ package org.thingsboard.server.controller;
17 17
18 import com.fasterxml.jackson.databind.JsonNode; 18 import com.fasterxml.jackson.databind.JsonNode;
19 import com.google.common.util.concurrent.ListenableFuture; 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import lombok.RequiredArgsConstructor;
20 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.http.HttpStatus; 22 import org.springframework.http.HttpStatus;
22 import org.springframework.http.ResponseEntity; 23 import org.springframework.http.ResponseEntity;
23 import org.springframework.security.access.prepost.PreAuthorize; 24 import org.springframework.security.access.prepost.PreAuthorize;
24 import org.springframework.web.bind.annotation.PathVariable; 25 import org.springframework.web.bind.annotation.PathVariable;
  26 +import org.springframework.web.bind.annotation.PostMapping;
25 import org.springframework.web.bind.annotation.RequestBody; 27 import org.springframework.web.bind.annotation.RequestBody;
26 import org.springframework.web.bind.annotation.RequestMapping; 28 import org.springframework.web.bind.annotation.RequestMapping;
27 import org.springframework.web.bind.annotation.RequestMethod; 29 import org.springframework.web.bind.annotation.RequestMethod;
@@ -52,10 +54,14 @@ import org.thingsboard.server.dao.exception.DataValidationException; @@ -52,10 +54,14 @@ import org.thingsboard.server.dao.exception.DataValidationException;
52 import org.thingsboard.server.dao.exception.IncorrectParameterException; 54 import org.thingsboard.server.dao.exception.IncorrectParameterException;
53 import org.thingsboard.server.dao.model.ModelConstants; 55 import org.thingsboard.server.dao.model.ModelConstants;
54 import org.thingsboard.server.queue.util.TbCoreComponent; 56 import org.thingsboard.server.queue.util.TbCoreComponent;
  57 +import org.thingsboard.server.service.edge.EdgeBulkImportService;
  58 +import org.thingsboard.server.service.importing.BulkImportRequest;
  59 +import org.thingsboard.server.service.importing.BulkImportResult;
55 import org.thingsboard.server.service.security.model.SecurityUser; 60 import org.thingsboard.server.service.security.model.SecurityUser;
56 import org.thingsboard.server.service.security.permission.Operation; 61 import org.thingsboard.server.service.security.permission.Operation;
57 import org.thingsboard.server.service.security.permission.Resource; 62 import org.thingsboard.server.service.security.permission.Resource;
58 63
  64 +import java.io.IOException;
59 import java.util.ArrayList; 65 import java.util.ArrayList;
60 import java.util.List; 66 import java.util.List;
61 import java.util.stream.Collectors; 67 import java.util.stream.Collectors;
@@ -64,7 +70,9 @@ import java.util.stream.Collectors; @@ -64,7 +70,9 @@ import java.util.stream.Collectors;
64 @TbCoreComponent 70 @TbCoreComponent
65 @Slf4j 71 @Slf4j
66 @RequestMapping("/api") 72 @RequestMapping("/api")
  73 +@RequiredArgsConstructor
67 public class EdgeController extends BaseController { 74 public class EdgeController extends BaseController {
  75 + private final EdgeBulkImportService edgeBulkImportService;
68 76
69 public static final String EDGE_ID = "edgeId"; 77 public static final String EDGE_ID = "edgeId";
70 78
@@ -132,17 +140,8 @@ public class EdgeController extends BaseController { @@ -132,17 +140,8 @@ public class EdgeController extends BaseController {
132 edge.getId(), edge); 140 edge.getId(), edge);
133 141
134 Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true)); 142 Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true));
  143 + onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created);
135 144
136 - if (created) {  
137 - ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), savedEdge.getId());  
138 - edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());  
139 - edgeService.assignDefaultRuleChainsToEdge(tenantId, savedEdge.getId());  
140 - }  
141 -  
142 - tbClusterService.broadcastEntityStateChangeEvent(savedEdge.getTenantId(), savedEdge.getId(),  
143 - created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);  
144 -  
145 - logEntityAction(savedEdge.getId(), savedEdge, null, created ? ActionType.ADDED : ActionType.UPDATED, null);  
146 return savedEdge; 145 return savedEdge;
147 } catch (Exception e) { 146 } catch (Exception e) {
148 logEntityAction(emptyId(EntityType.EDGE), edge, 147 logEntityAction(emptyId(EntityType.EDGE), edge,
@@ -151,6 +150,19 @@ public class EdgeController extends BaseController { @@ -151,6 +150,19 @@ public class EdgeController extends BaseController {
151 } 150 }
152 } 151 }
153 152
  153 + private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated) throws IOException, ThingsboardException {
  154 + if (!updated) {
  155 + ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId());
  156 + edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId());
  157 + edgeService.assignDefaultRuleChainsToEdge(tenantId, edge.getId());
  158 + }
  159 +
  160 + tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(),
  161 + updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED);
  162 +
  163 + logEntityAction(edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
  164 + }
  165 +
154 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 166 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
155 @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) 167 @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE)
156 @ResponseStatus(value = HttpStatus.OK) 168 @ResponseStatus(value = HttpStatus.OK)
@@ -563,6 +575,24 @@ public class EdgeController extends BaseController { @@ -563,6 +575,24 @@ public class EdgeController extends BaseController {
563 } 575 }
564 } 576 }
565 577
  578 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
  579 + @PostMapping("/edge/bulk_import")
  580 + public BulkImportResult<Edge> processEdgeBulkImport(@RequestBody BulkImportRequest request) throws Exception {
  581 + SecurityUser user = getCurrentUser();
  582 + RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId());
  583 + if (edgeTemplateRootRuleChain == null) {
  584 + throw new DataValidationException("Root edge rule chain is not available!");
  585 + }
  586 +
  587 + return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
  588 + try {
  589 + onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated());
  590 + } catch (Exception e) {
  591 + throw new RuntimeException(e);
  592 + }
  593 + });
  594 + }
  595 +
566 private void cleanUpLicenseKey(Edge edge) { 596 private void cleanUpLicenseKey(Edge edge) {
567 edge.setEdgeLicenseKey(null); 597 edge.setEdgeLicenseKey(null);
568 } 598 }
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java renamed from application/src/main/java/org/thingsboard/server/service/action/RuleEngineEntityActionService.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.HasName; @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.HasName;
28 import org.thingsboard.server.common.data.HasTenantId; 28 import org.thingsboard.server.common.data.HasTenantId;
29 import org.thingsboard.server.common.data.User; 29 import org.thingsboard.server.common.data.User;
30 import org.thingsboard.server.common.data.audit.ActionType; 30 import org.thingsboard.server.common.data.audit.ActionType;
  31 +import org.thingsboard.server.common.data.exception.ThingsboardException;
31 import org.thingsboard.server.common.data.id.CustomerId; 32 import org.thingsboard.server.common.data.id.CustomerId;
32 import org.thingsboard.server.common.data.id.EntityId; 33 import org.thingsboard.server.common.data.id.EntityId;
33 import org.thingsboard.server.common.data.id.TenantId; 34 import org.thingsboard.server.common.data.id.TenantId;
@@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
38 import org.thingsboard.server.common.msg.TbMsg; 39 import org.thingsboard.server.common.msg.TbMsg;
39 import org.thingsboard.server.common.msg.TbMsgDataType; 40 import org.thingsboard.server.common.msg.TbMsgDataType;
40 import org.thingsboard.server.common.msg.TbMsgMetaData; 41 import org.thingsboard.server.common.msg.TbMsgMetaData;
  42 +import org.thingsboard.server.dao.audit.AuditLogService;
41 import org.thingsboard.server.queue.util.TbCoreComponent; 43 import org.thingsboard.server.queue.util.TbCoreComponent;
42 import org.thingsboard.server.cluster.TbClusterService; 44 import org.thingsboard.server.cluster.TbClusterService;
43 45
@@ -49,8 +51,9 @@ import java.util.stream.Collectors; @@ -49,8 +51,9 @@ import java.util.stream.Collectors;
49 @Service 51 @Service
50 @RequiredArgsConstructor 52 @RequiredArgsConstructor
51 @Slf4j 53 @Slf4j
52 -public class RuleEngineEntityActionService { 54 +public class EntityActionService {
53 private final TbClusterService tbClusterService; 55 private final TbClusterService tbClusterService;
  56 + private final AuditLogService auditLogService;
54 57
55 private static final ObjectMapper json = new ObjectMapper(); 58 private static final ObjectMapper json = new ObjectMapper();
56 59
@@ -209,6 +212,17 @@ public class RuleEngineEntityActionService { @@ -209,6 +212,17 @@ public class RuleEngineEntityActionService {
209 } 212 }
210 } 213 }
211 214
  215 + public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
  216 + ActionType actionType, Exception e, Object... additionalInfo) {
  217 + if (customerId == null || customerId.isNullUid()) {
  218 + customerId = user.getCustomerId();
  219 + }
  220 + if (e == null) {
  221 + pushEntityActionToRuleEngine(entityId, entity, user.getTenantId(), customerId, actionType, user, additionalInfo);
  222 + }
  223 + auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
  224 + }
  225 +
212 226
213 private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) { 227 private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
214 T result = null; 228 T result = null;
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.asset;
  17 +
  18 +import com.fasterxml.jackson.databind.node.ObjectNode;
  19 +import com.fasterxml.jackson.databind.node.TextNode;
  20 +import org.springframework.stereotype.Service;
  21 +import org.thingsboard.common.util.JacksonUtil;
  22 +import org.thingsboard.server.cluster.TbClusterService;
  23 +import org.thingsboard.server.common.data.asset.Asset;
  24 +import org.thingsboard.server.dao.asset.AssetService;
  25 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  26 +import org.thingsboard.server.queue.util.TbCoreComponent;
  27 +import org.thingsboard.server.service.action.EntityActionService;
  28 +import org.thingsboard.server.service.importing.AbstractBulkImportService;
  29 +import org.thingsboard.server.service.importing.BulkImportColumnType;
  30 +import org.thingsboard.server.service.importing.BulkImportRequest;
  31 +import org.thingsboard.server.service.importing.ImportedEntityInfo;
  32 +import org.thingsboard.server.service.security.AccessValidator;
  33 +import org.thingsboard.server.service.security.model.SecurityUser;
  34 +import org.thingsboard.server.service.security.permission.AccessControlService;
  35 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  36 +
  37 +import java.util.Map;
  38 +import java.util.Optional;
  39 +
  40 +@Service
  41 +@TbCoreComponent
  42 +public class AssetBulkImportService extends AbstractBulkImportService<Asset> {
  43 + private final AssetService assetService;
  44 +
  45 + public AssetBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
  46 + AccessControlService accessControlService, AccessValidator accessValidator,
  47 + EntityActionService entityActionService, TbClusterService clusterService, AssetService assetService) {
  48 + super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
  49 + this.assetService = assetService;
  50 + }
  51 +
  52 + @Override
  53 + protected ImportedEntityInfo<Asset> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
  54 + ImportedEntityInfo<Asset> importedEntityInfo = new ImportedEntityInfo<>();
  55 +
  56 + Asset asset = new Asset();
  57 + asset.setTenantId(user.getTenantId());
  58 + setAssetFields(asset, fields);
  59 +
  60 + Asset existingAsset = assetService.findAssetByTenantIdAndName(user.getTenantId(), asset.getName());
  61 + if (existingAsset != null && importRequest.getMapping().getUpdate()) {
  62 + importedEntityInfo.setOldEntity(new Asset(existingAsset));
  63 + importedEntityInfo.setUpdated(true);
  64 + existingAsset.update(asset);
  65 + asset = existingAsset;
  66 + }
  67 + asset = assetService.saveAsset(asset);
  68 +
  69 + importedEntityInfo.setEntity(asset);
  70 + return importedEntityInfo;
  71 + }
  72 +
  73 + private void setAssetFields(Asset asset, Map<BulkImportColumnType, String> fields) {
  74 + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(asset.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
  75 + fields.forEach((columnType, value) -> {
  76 + switch (columnType) {
  77 + case NAME:
  78 + asset.setName(value);
  79 + break;
  80 + case TYPE:
  81 + asset.setType(value);
  82 + break;
  83 + case LABEL:
  84 + asset.setLabel(value);
  85 + break;
  86 + case DESCRIPTION:
  87 + additionalInfo.set("description", new TextNode(value));
  88 + break;
  89 + }
  90 + });
  91 + asset.setAdditionalInfo(additionalInfo);
  92 + }
  93 +
  94 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.device;
  17 +
  18 +import com.fasterxml.jackson.databind.node.BooleanNode;
  19 +import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import com.fasterxml.jackson.databind.node.TextNode;
  21 +import lombok.SneakyThrows;
  22 +import org.apache.commons.collections.CollectionUtils;
  23 +import org.apache.commons.lang3.RandomStringUtils;
  24 +import org.apache.commons.lang3.StringUtils;
  25 +import org.springframework.stereotype.Service;
  26 +import org.thingsboard.common.util.JacksonUtil;
  27 +import org.thingsboard.server.cluster.TbClusterService;
  28 +import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.DeviceProfile;
  30 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  31 +import org.thingsboard.server.common.data.DeviceProfileType;
  32 +import org.thingsboard.server.common.data.DeviceTransportType;
  33 +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
  34 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
  35 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
  36 +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
  37 +import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
  38 +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
  39 +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
  40 +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
  41 +import org.thingsboard.server.common.data.id.TenantId;
  42 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  43 +import org.thingsboard.server.common.data.security.DeviceCredentialsType;
  44 +import org.thingsboard.server.dao.device.DeviceCredentialsService;
  45 +import org.thingsboard.server.dao.device.DeviceProfileService;
  46 +import org.thingsboard.server.dao.device.DeviceService;
  47 +import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  49 +import org.thingsboard.server.queue.util.TbCoreComponent;
  50 +import org.thingsboard.server.service.action.EntityActionService;
  51 +import org.thingsboard.server.service.importing.AbstractBulkImportService;
  52 +import org.thingsboard.server.service.importing.BulkImportColumnType;
  53 +import org.thingsboard.server.service.importing.BulkImportRequest;
  54 +import org.thingsboard.server.service.importing.ImportedEntityInfo;
  55 +import org.thingsboard.server.service.security.AccessValidator;
  56 +import org.thingsboard.server.service.security.model.SecurityUser;
  57 +import org.thingsboard.server.service.security.permission.AccessControlService;
  58 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  59 +
  60 +import java.util.Collection;
  61 +import java.util.EnumSet;
  62 +import java.util.Map;
  63 +import java.util.Objects;
  64 +import java.util.Optional;
  65 +import java.util.Set;
  66 +
  67 +@Service
  68 +@TbCoreComponent
  69 +public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
  70 + protected final DeviceService deviceService;
  71 + protected final DeviceCredentialsService deviceCredentialsService;
  72 + protected final DeviceProfileService deviceProfileService;
  73 +
  74 + public DeviceBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
  75 + AccessControlService accessControlService, AccessValidator accessValidator,
  76 + EntityActionService entityActionService, TbClusterService clusterService,
  77 + DeviceService deviceService, DeviceCredentialsService deviceCredentialsService,
  78 + DeviceProfileService deviceProfileService) {
  79 + super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
  80 + this.deviceService = deviceService;
  81 + this.deviceCredentialsService = deviceCredentialsService;
  82 + this.deviceProfileService = deviceProfileService;
  83 + }
  84 +
  85 + @Override
  86 + protected ImportedEntityInfo<Device> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
  87 + ImportedEntityInfo<Device> importedEntityInfo = new ImportedEntityInfo<>();
  88 +
  89 + Device device = new Device();
  90 + device.setTenantId(user.getTenantId());
  91 + setDeviceFields(device, fields);
  92 +
  93 + Device existingDevice = deviceService.findDeviceByTenantIdAndName(user.getTenantId(), device.getName());
  94 + if (existingDevice != null && importRequest.getMapping().getUpdate()) {
  95 + importedEntityInfo.setOldEntity(new Device(existingDevice));
  96 + importedEntityInfo.setUpdated(true);
  97 + existingDevice.updateDevice(device);
  98 + device = existingDevice;
  99 + }
  100 +
  101 + DeviceCredentials deviceCredentials;
  102 + try {
  103 + deviceCredentials = createDeviceCredentials(fields);
  104 + deviceCredentialsService.formatCredentials(deviceCredentials);
  105 + } catch (Exception e) {
  106 + throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
  107 + }
  108 +
  109 + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
  110 + setUpLwM2mDeviceProfile(user.getTenantId(), device);
  111 + }
  112 +
  113 + device = deviceService.saveDeviceWithCredentials(device, deviceCredentials);
  114 +
  115 + importedEntityInfo.setEntity(device);
  116 + return importedEntityInfo;
  117 + }
  118 +
  119 + private void setDeviceFields(Device device, Map<BulkImportColumnType, String> fields) {
  120 + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(device.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
  121 + fields.forEach((columnType, value) -> {
  122 + switch (columnType) {
  123 + case NAME:
  124 + device.setName(value);
  125 + break;
  126 + case TYPE:
  127 + device.setType(value);
  128 + break;
  129 + case LABEL:
  130 + device.setLabel(value);
  131 + break;
  132 + case DESCRIPTION:
  133 + additionalInfo.set("description", new TextNode(value));
  134 + break;
  135 + case IS_GATEWAY:
  136 + additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value)));
  137 + break;
  138 + }
  139 + device.setAdditionalInfo(additionalInfo);
  140 + });
  141 + }
  142 +
  143 + @SneakyThrows
  144 + private DeviceCredentials createDeviceCredentials(Map<BulkImportColumnType, String> fields) {
  145 + DeviceCredentials credentials = new DeviceCredentials();
  146 + if (fields.containsKey(BulkImportColumnType.LWM2M_CLIENT_ENDPOINT)) {
  147 + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
  148 + setUpLwm2mCredentials(fields, credentials);
  149 + } else if (fields.containsKey(BulkImportColumnType.X509)) {
  150 + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE);
  151 + setUpX509CertificateCredentials(fields, credentials);
  152 + } else if (CollectionUtils.containsAny(fields.keySet(), EnumSet.of(BulkImportColumnType.MQTT_CLIENT_ID, BulkImportColumnType.MQTT_USER_NAME, BulkImportColumnType.MQTT_PASSWORD))) {
  153 + credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC);
  154 + setUpBasicMqttCredentials(fields, credentials);
  155 + } else {
  156 + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
  157 + setUpAccessTokenCredentials(fields, credentials);
  158 + }
  159 + return credentials;
  160 + }
  161 +
  162 + private void setUpAccessTokenCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
  163 + credentials.setCredentialsId(Optional.ofNullable(fields.get(BulkImportColumnType.ACCESS_TOKEN))
  164 + .orElseGet(() -> RandomStringUtils.randomAlphanumeric(20)));
  165 + }
  166 +
  167 + private void setUpBasicMqttCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
  168 + BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials();
  169 + basicMqttCredentials.setClientId(fields.get(BulkImportColumnType.MQTT_CLIENT_ID));
  170 + basicMqttCredentials.setUserName(fields.get(BulkImportColumnType.MQTT_USER_NAME));
  171 + basicMqttCredentials.setPassword(fields.get(BulkImportColumnType.MQTT_PASSWORD));
  172 + credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials));
  173 + }
  174 +
  175 + private void setUpX509CertificateCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) {
  176 + credentials.setCredentialsValue(fields.get(BulkImportColumnType.X509));
  177 + }
  178 +
  179 + private void setUpLwm2mCredentials(Map<BulkImportColumnType, String> fields, DeviceCredentials credentials) throws com.fasterxml.jackson.core.JsonProcessingException {
  180 + ObjectNode lwm2mCredentials = JacksonUtil.newObjectNode();
  181 +
  182 + Set.of(BulkImportColumnType.LWM2M_CLIENT_SECURITY_CONFIG_MODE, BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE,
  183 + BulkImportColumnType.LWM2M_SERVER_SECURITY_MODE).stream()
  184 + .map(fields::get)
  185 + .filter(Objects::nonNull)
  186 + .forEach(securityMode -> {
  187 + try {
  188 + LwM2MSecurityMode.valueOf(securityMode.toUpperCase());
  189 + } catch (IllegalArgumentException e) {
  190 + throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode + ", (the mode should be: NO_SEC, PSK, RPK, X509)!");
  191 + }
  192 + });
  193 +
  194 + ObjectNode client = JacksonUtil.newObjectNode();
  195 + setValues(client, fields, Set.of(BulkImportColumnType.LWM2M_CLIENT_SECURITY_CONFIG_MODE,
  196 + BulkImportColumnType.LWM2M_CLIENT_ENDPOINT, BulkImportColumnType.LWM2M_CLIENT_IDENTITY,
  197 + BulkImportColumnType.LWM2M_CLIENT_KEY, BulkImportColumnType.LWM2M_CLIENT_CERT));
  198 + LwM2MClientCredentials lwM2MClientCredentials = JacksonUtil.treeToValue(client, LwM2MClientCredentials.class);
  199 + // so that only fields needed for specific type of lwM2MClientCredentials were saved in json
  200 + lwm2mCredentials.set("client", JacksonUtil.valueToTree(lwM2MClientCredentials));
  201 +
  202 + ObjectNode bootstrapServer = JacksonUtil.newObjectNode();
  203 + setValues(bootstrapServer, fields, Set.of(BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE,
  204 + BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID, BulkImportColumnType.LWM2M_BOOTSTRAP_SERVER_SECRET_KEY));
  205 +
  206 + ObjectNode lwm2mServer = JacksonUtil.newObjectNode();
  207 + setValues(lwm2mServer, fields, Set.of(BulkImportColumnType.LWM2M_SERVER_SECURITY_MODE,
  208 + BulkImportColumnType.LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID, BulkImportColumnType.LWM2M_SERVER_CLIENT_SECRET_KEY));
  209 +
  210 + ObjectNode bootstrap = JacksonUtil.newObjectNode();
  211 + bootstrap.set("bootstrapServer", bootstrapServer);
  212 + bootstrap.set("lwm2mServer", lwm2mServer);
  213 + lwm2mCredentials.set("bootstrap", bootstrap);
  214 +
  215 + credentials.setCredentialsValue(lwm2mCredentials.toString());
  216 + }
  217 +
  218 + private void setUpLwM2mDeviceProfile(TenantId tenantId, Device device) {
  219 + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType());
  220 + if (deviceProfile != null) {
  221 + if (deviceProfile.getTransportType() != DeviceTransportType.LWM2M) {
  222 + deviceProfile.setTransportType(DeviceTransportType.LWM2M);
  223 + deviceProfile.getProfileData().setTransportConfiguration(new Lwm2mDeviceProfileTransportConfiguration());
  224 + deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
  225 + device.setDeviceProfileId(deviceProfile.getId());
  226 + }
  227 + } else {
  228 + deviceProfile = new DeviceProfile();
  229 + deviceProfile.setTenantId(tenantId);
  230 + deviceProfile.setType(DeviceProfileType.DEFAULT);
  231 + deviceProfile.setName(device.getType());
  232 + deviceProfile.setTransportType(DeviceTransportType.LWM2M);
  233 + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
  234 +
  235 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  236 + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
  237 + DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
  238 + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
  239 +
  240 + deviceProfileData.setConfiguration(configuration);
  241 + deviceProfileData.setTransportConfiguration(transportConfiguration);
  242 + deviceProfileData.setProvisionConfiguration(provisionConfiguration);
  243 + deviceProfile.setProfileData(deviceProfileData);
  244 +
  245 + deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
  246 + device.setDeviceProfileId(deviceProfile.getId());
  247 + }
  248 + }
  249 +
  250 + private void setValues(ObjectNode objectNode, Map<BulkImportColumnType, String> data, Collection<BulkImportColumnType> columns) {
  251 + for (BulkImportColumnType column : columns) {
  252 + String value = StringUtils.defaultString(data.get(column), column.getDefaultValue());
  253 + if (value != null && column.getKey() != null) {
  254 + objectNode.set(column.getKey(), new TextNode(value));
  255 + }
  256 + }
  257 + }
  258 +
  259 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.edge;
  17 +
  18 +import com.fasterxml.jackson.databind.node.ObjectNode;
  19 +import com.fasterxml.jackson.databind.node.TextNode;
  20 +import org.springframework.stereotype.Service;
  21 +import org.thingsboard.common.util.JacksonUtil;
  22 +import org.thingsboard.server.cluster.TbClusterService;
  23 +import org.thingsboard.server.common.data.edge.Edge;
  24 +import org.thingsboard.server.dao.edge.EdgeService;
  25 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  26 +import org.thingsboard.server.queue.util.TbCoreComponent;
  27 +import org.thingsboard.server.service.action.EntityActionService;
  28 +import org.thingsboard.server.service.importing.AbstractBulkImportService;
  29 +import org.thingsboard.server.service.importing.BulkImportColumnType;
  30 +import org.thingsboard.server.service.importing.BulkImportRequest;
  31 +import org.thingsboard.server.service.importing.ImportedEntityInfo;
  32 +import org.thingsboard.server.service.security.AccessValidator;
  33 +import org.thingsboard.server.service.security.model.SecurityUser;
  34 +import org.thingsboard.server.service.security.permission.AccessControlService;
  35 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  36 +
  37 +import java.util.Map;
  38 +import java.util.Optional;
  39 +
  40 +@Service
  41 +@TbCoreComponent
  42 +public class EdgeBulkImportService extends AbstractBulkImportService<Edge> {
  43 + private final EdgeService edgeService;
  44 +
  45 + public EdgeBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache,
  46 + AccessControlService accessControlService, AccessValidator accessValidator,
  47 + EntityActionService entityActionService, TbClusterService clusterService, EdgeService edgeService) {
  48 + super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService);
  49 + this.edgeService = edgeService;
  50 + }
  51 +
  52 + @Override
  53 + protected ImportedEntityInfo<Edge> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user) {
  54 + ImportedEntityInfo<Edge> importedEntityInfo = new ImportedEntityInfo<>();
  55 +
  56 + Edge edge = new Edge();
  57 + edge.setTenantId(user.getTenantId());
  58 + setEdgeFields(edge, fields);
  59 +
  60 + Edge existingEdge = edgeService.findEdgeByTenantIdAndName(user.getTenantId(), edge.getName());
  61 + if (existingEdge != null && importRequest.getMapping().getUpdate()) {
  62 + importedEntityInfo.setOldEntity(new Edge(existingEdge));
  63 + importedEntityInfo.setUpdated(true);
  64 + existingEdge.update(edge);
  65 + edge = existingEdge;
  66 + }
  67 + edge = edgeService.saveEdge(edge, true);
  68 +
  69 + importedEntityInfo.setEntity(edge);
  70 + return importedEntityInfo;
  71 + }
  72 +
  73 + private void setEdgeFields(Edge edge, Map<BulkImportColumnType, String> fields) {
  74 + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(edge.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode);
  75 + fields.forEach((columnType, value) -> {
  76 + switch (columnType) {
  77 + case NAME:
  78 + edge.setName(value);
  79 + break;
  80 + case TYPE:
  81 + edge.setType(value);
  82 + break;
  83 + case LABEL:
  84 + edge.setLabel(value);
  85 + break;
  86 + case DESCRIPTION:
  87 + additionalInfo.set("description", new TextNode(value));
  88 + break;
  89 + case EDGE_LICENSE_KEY:
  90 + edge.setEdgeLicenseKey(value);
  91 + break;
  92 + case CLOUD_ENDPOINT:
  93 + edge.setCloudEndpoint(value);
  94 + break;
  95 + case ROUTING_KEY:
  96 + edge.setRoutingKey(value);
  97 + break;
  98 + case SECRET:
  99 + edge.setSecret(value);
  100 + break;
  101 + }
  102 + });
  103 + edge.setAdditionalInfo(additionalInfo);
  104 + }
  105 +
  106 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.importing;
  17 +
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import com.google.gson.JsonObject;
  20 +import com.google.gson.JsonPrimitive;
  21 +import lombok.Data;
  22 +import lombok.RequiredArgsConstructor;
  23 +import lombok.SneakyThrows;
  24 +import org.apache.commons.lang3.StringUtils;
  25 +import org.thingsboard.server.cluster.TbClusterService;
  26 +import org.thingsboard.server.common.data.BaseData;
  27 +import org.thingsboard.server.common.data.TenantProfile;
  28 +import org.thingsboard.server.common.data.audit.ActionType;
  29 +import org.thingsboard.server.common.data.id.EntityId;
  30 +import org.thingsboard.server.common.data.id.UUIDBased;
  31 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  32 +import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
  33 +import org.thingsboard.server.common.data.kv.DataType;
  34 +import org.thingsboard.server.common.data.kv.TsKvEntry;
  35 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
  36 +import org.thingsboard.server.common.transport.adaptor.JsonConverter;
  37 +import org.thingsboard.server.controller.BaseController;
  38 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  39 +import org.thingsboard.server.service.action.EntityActionService;
  40 +import org.thingsboard.server.service.importing.BulkImportRequest.ColumnMapping;
  41 +import org.thingsboard.server.service.security.AccessValidator;
  42 +import org.thingsboard.server.service.security.model.SecurityUser;
  43 +import org.thingsboard.server.service.security.permission.AccessControlService;
  44 +import org.thingsboard.server.service.security.permission.Operation;
  45 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
  46 +import org.thingsboard.server.utils.CsvUtils;
  47 +import org.thingsboard.server.utils.TypeCastUtil;
  48 +
  49 +import javax.annotation.Nullable;
  50 +import java.util.ArrayList;
  51 +import java.util.Arrays;
  52 +import java.util.LinkedHashMap;
  53 +import java.util.List;
  54 +import java.util.Map;
  55 +import java.util.concurrent.TimeUnit;
  56 +import java.util.concurrent.atomic.AtomicInteger;
  57 +import java.util.function.Consumer;
  58 +import java.util.stream.Collectors;
  59 +import java.util.stream.Stream;
  60 +
  61 +@RequiredArgsConstructor
  62 +public abstract class AbstractBulkImportService<E extends BaseData<? extends EntityId>> {
  63 + protected final TelemetrySubscriptionService tsSubscriptionService;
  64 + protected final TbTenantProfileCache tenantProfileCache;
  65 + protected final AccessControlService accessControlService;
  66 + protected final AccessValidator accessValidator;
  67 + protected final EntityActionService entityActionService;
  68 + protected final TbClusterService clusterService;
  69 +
  70 + public final BulkImportResult<E> processBulkImport(BulkImportRequest request, SecurityUser user, Consumer<ImportedEntityInfo<E>> onEntityImported) throws Exception {
  71 + BulkImportResult<E> result = new BulkImportResult<>();
  72 +
  73 + AtomicInteger i = new AtomicInteger(0);
  74 + if (request.getMapping().getHeader()) {
  75 + i.incrementAndGet();
  76 + }
  77 +
  78 + parseData(request).forEach(entityData -> {
  79 + i.incrementAndGet();
  80 + try {
  81 + ImportedEntityInfo<E> importedEntityInfo = saveEntity(request, entityData.getFields(), user);
  82 + onEntityImported.accept(importedEntityInfo);
  83 +
  84 + E entity = importedEntityInfo.getEntity();
  85 +
  86 + saveKvs(user, entity, entityData.getKvs());
  87 +
  88 + if (importedEntityInfo.getRelatedError() != null) {
  89 + throw new RuntimeException(importedEntityInfo.getRelatedError());
  90 + }
  91 +
  92 + if (importedEntityInfo.isUpdated()) {
  93 + result.setUpdated(result.getUpdated() + 1);
  94 + } else {
  95 + result.setCreated(result.getCreated() + 1);
  96 + }
  97 + } catch (Exception e) {
  98 + result.setErrors(result.getErrors() + 1);
  99 + result.getErrorsList().add(String.format("Line %d: %s", i.get(), e.getMessage()));
  100 + }
  101 + });
  102 +
  103 + return result;
  104 + }
  105 +
  106 + protected abstract ImportedEntityInfo<E> saveEntity(BulkImportRequest importRequest, Map<BulkImportColumnType, String> fields, SecurityUser user);
  107 +
  108 + /*
  109 + * Attributes' values are firstly added to JsonObject in order to then make some type cast,
  110 + * because we get all values as strings from CSV
  111 + * */
  112 + private void saveKvs(SecurityUser user, E entity, Map<ColumnMapping, ParsedValue> data) {
  113 + Arrays.stream(BulkImportColumnType.values())
  114 + .filter(BulkImportColumnType::isKv)
  115 + .map(kvType -> {
  116 + JsonObject kvs = new JsonObject();
  117 + data.entrySet().stream()
  118 + .filter(dataEntry -> dataEntry.getKey().getType() == kvType &&
  119 + StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
  120 + .forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive()));
  121 + return Map.entry(kvType, kvs);
  122 + })
  123 + .filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)
  124 + .forEach(kvsEntry -> {
  125 + BulkImportColumnType kvType = kvsEntry.getKey();
  126 + if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) {
  127 + saveAttributes(user, entity, kvsEntry, kvType);
  128 + } else {
  129 + saveTelemetry(user, entity, kvsEntry);
  130 + }
  131 + });
  132 + }
  133 +
  134 + @SneakyThrows
  135 + private void saveTelemetry(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry) {
  136 + List<TsKvEntry> timeseries = JsonConverter.convertToTelemetry(kvsEntry.getValue(), System.currentTimeMillis())
  137 + .entrySet().stream()
  138 + .flatMap(entry -> entry.getValue().stream().map(kvEntry -> new BasicTsKvEntry(entry.getKey(), kvEntry)))
  139 + .collect(Collectors.toList());
  140 +
  141 + accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entity.getId(), (result, tenantId, entityId) -> {
  142 + TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
  143 + long tenantTtl = TimeUnit.DAYS.toSeconds(((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()).getDefaultStorageTtlDays());
  144 + tsSubscriptionService.saveAndNotify(tenantId, user.getCustomerId(), entityId, timeseries, tenantTtl, new FutureCallback<Void>() {
  145 + @Override
  146 + public void onSuccess(@Nullable Void tmp) {
  147 + entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null, null,
  148 + ActionType.TIMESERIES_UPDATED, null, timeseries);
  149 + }
  150 +
  151 + @Override
  152 + public void onFailure(Throwable t) {
  153 + entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null, null,
  154 + ActionType.TIMESERIES_UPDATED, BaseController.toException(t), timeseries);
  155 + throw new RuntimeException(t);
  156 + }
  157 + });
  158 + });
  159 + }
  160 +
  161 + @SneakyThrows
  162 + private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) {
  163 + String scope = kvType.getKey();
  164 + List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue()));
  165 +
  166 + accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> {
  167 + tsSubscriptionService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback<>() {
  168 +
  169 + @Override
  170 + public void onSuccess(Void unused) {
  171 + entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
  172 + null, ActionType.ATTRIBUTES_UPDATED, null, scope, attributes);
  173 + }
  174 +
  175 + @Override
  176 + public void onFailure(Throwable throwable) {
  177 + entityActionService.logEntityAction(user, (UUIDBased & EntityId) entityId, null,
  178 + null, ActionType.ATTRIBUTES_UPDATED, BaseController.toException(throwable),
  179 + scope, attributes);
  180 + throw new RuntimeException(throwable);
  181 + }
  182 +
  183 + });
  184 + });
  185 + }
  186 +
  187 + private List<EntityData> parseData(BulkImportRequest request) throws Exception {
  188 + List<List<String>> records = CsvUtils.parseCsv(request.getFile(), request.getMapping().getDelimiter());
  189 + if (request.getMapping().getHeader()) {
  190 + records.remove(0);
  191 + }
  192 +
  193 + List<ColumnMapping> columnsMappings = request.getMapping().getColumns();
  194 + return records.stream()
  195 + .map(record -> {
  196 + EntityData entityData = new EntityData();
  197 + Stream.iterate(0, i -> i < record.size(), i -> i + 1)
  198 + .map(i -> Map.entry(columnsMappings.get(i), record.get(i)))
  199 + .filter(entry -> StringUtils.isNotEmpty(entry.getValue()))
  200 + .forEach(entry -> {
  201 + if (!entry.getKey().getType().isKv()) {
  202 + entityData.getFields().put(entry.getKey().getType(), entry.getValue());
  203 + } else {
  204 + Map.Entry<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
  205 + entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
  206 + }
  207 + });
  208 + return entityData;
  209 + })
  210 + .collect(Collectors.toList());
  211 + }
  212 +
  213 + @Data
  214 + protected static class EntityData {
  215 + private final Map<BulkImportColumnType, String> fields = new LinkedHashMap<>();
  216 + private final Map<ColumnMapping, ParsedValue> kvs = new LinkedHashMap<>();
  217 + }
  218 +
  219 + @Data
  220 + protected static class ParsedValue {
  221 + private final Object value;
  222 + private final DataType dataType;
  223 +
  224 + public JsonPrimitive toJsonPrimitive() {
  225 + switch (dataType) {
  226 + case STRING:
  227 + return new JsonPrimitive((String) value);
  228 + case LONG:
  229 + return new JsonPrimitive((Long) value);
  230 + case DOUBLE:
  231 + return new JsonPrimitive((Double) value);
  232 + case BOOLEAN:
  233 + return new JsonPrimitive((Boolean) value);
  234 + default:
  235 + return null;
  236 + }
  237 + }
  238 +
  239 + public String stringValue() {
  240 + return value.toString();
  241 + }
  242 +
  243 + }
  244 +
  245 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.importing;
  17 +
  18 +import lombok.Getter;
  19 +import org.thingsboard.server.common.data.DataConstants;
  20 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
  21 +
  22 +@Getter
  23 +public enum BulkImportColumnType {
  24 + NAME,
  25 + TYPE,
  26 + LABEL,
  27 + SHARED_ATTRIBUTE(DataConstants.SHARED_SCOPE, true),
  28 + SERVER_ATTRIBUTE(DataConstants.SERVER_SCOPE, true),
  29 + TIMESERIES(true),
  30 + ACCESS_TOKEN,
  31 + X509,
  32 + MQTT_CLIENT_ID,
  33 + MQTT_USER_NAME,
  34 + MQTT_PASSWORD,
  35 + LWM2M_CLIENT_ENDPOINT("endpoint"),
  36 + LWM2M_CLIENT_SECURITY_CONFIG_MODE("securityConfigClientMode", LwM2MSecurityMode.NO_SEC.name()),
  37 + LWM2M_CLIENT_IDENTITY("identity"),
  38 + LWM2M_CLIENT_KEY("key"),
  39 + LWM2M_CLIENT_CERT("cert"),
  40 + LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
  41 + LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
  42 + LWM2M_BOOTSTRAP_SERVER_SECRET_KEY("clientSecretKey"),
  43 + LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
  44 + LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
  45 + LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"),
  46 + IS_GATEWAY,
  47 + DESCRIPTION,
  48 + EDGE_LICENSE_KEY,
  49 + CLOUD_ENDPOINT,
  50 + ROUTING_KEY,
  51 + SECRET;
  52 +
  53 + private String key;
  54 + private String defaultValue;
  55 + private boolean isKv = false;
  56 +
  57 + BulkImportColumnType() {
  58 + }
  59 +
  60 + BulkImportColumnType(String key) {
  61 + this.key = key;
  62 + }
  63 +
  64 + BulkImportColumnType(String key, String defaultValue) {
  65 + this.key = key;
  66 + this.defaultValue = defaultValue;
  67 + }
  68 +
  69 + BulkImportColumnType(boolean isKv) {
  70 + this.isKv = isKv;
  71 + }
  72 +
  73 + BulkImportColumnType(String key, boolean isKv) {
  74 + this.key = key;
  75 + this.isKv = isKv;
  76 + }
  77 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.importing;
  17 +
  18 +import lombok.Data;
  19 +
  20 +import java.util.List;
  21 +
  22 +@Data
  23 +public class BulkImportRequest {
  24 + private String file;
  25 + private Mapping mapping;
  26 +
  27 + @Data
  28 + public static class Mapping {
  29 + private List<ColumnMapping> columns;
  30 + private Character delimiter;
  31 + private Boolean update;
  32 + private Boolean header;
  33 + }
  34 +
  35 + @Data
  36 + public static class ColumnMapping {
  37 + private BulkImportColumnType type;
  38 + private String key;
  39 + }
  40 +
  41 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.importing;
  17 +
  18 +import lombok.Data;
  19 +
  20 +import java.util.LinkedList;
  21 +import java.util.List;
  22 +
  23 +@Data
  24 +public class BulkImportResult<E> {
  25 + private int created = 0;
  26 + private int updated = 0;
  27 + private int errors = 0;
  28 + private List<String> errorsList = new LinkedList<>();
  29 +
  30 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.importing;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class ImportedEntityInfo<E> {
  22 + private E entity;
  23 + private boolean isUpdated;
  24 + private E oldEntity;
  25 + private String relatedError;
  26 +}
@@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.id.AlarmId; @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.id.AlarmId;
26 import org.thingsboard.server.common.data.id.TenantId; 26 import org.thingsboard.server.common.data.id.TenantId;
27 import org.thingsboard.server.common.data.page.PageData; 27 import org.thingsboard.server.common.data.page.PageData;
28 import org.thingsboard.server.common.data.page.PageLink; 28 import org.thingsboard.server.common.data.page.PageLink;
29 -import org.thingsboard.server.common.data.page.SortOrder;  
30 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 29 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
31 import org.thingsboard.server.common.msg.queue.ServiceType; 30 import org.thingsboard.server.common.msg.queue.ServiceType;
32 import org.thingsboard.server.dao.alarm.AlarmDao; 31 import org.thingsboard.server.dao.alarm.AlarmDao;
@@ -34,17 +33,12 @@ import org.thingsboard.server.dao.alarm.AlarmService; @@ -34,17 +33,12 @@ import org.thingsboard.server.dao.alarm.AlarmService;
34 import org.thingsboard.server.dao.relation.RelationService; 33 import org.thingsboard.server.dao.relation.RelationService;
35 import org.thingsboard.server.dao.tenant.TbTenantProfileCache; 34 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
36 import org.thingsboard.server.dao.tenant.TenantDao; 35 import org.thingsboard.server.dao.tenant.TenantDao;
37 -import org.thingsboard.server.dao.util.PsqlDao;  
38 import org.thingsboard.server.queue.discovery.PartitionService; 36 import org.thingsboard.server.queue.discovery.PartitionService;
39 import org.thingsboard.server.queue.util.TbCoreComponent; 37 import org.thingsboard.server.queue.util.TbCoreComponent;
40 -import org.thingsboard.server.service.action.RuleEngineEntityActionService;  
41 -import org.thingsboard.server.service.ttl.AbstractCleanUpService; 38 +import org.thingsboard.server.service.action.EntityActionService;
42 39
43 -import java.sql.Connection;  
44 -import java.sql.SQLException;  
45 import java.util.Date; 40 import java.util.Date;
46 import java.util.Optional; 41 import java.util.Optional;
47 -import java.util.UUID;  
48 import java.util.concurrent.TimeUnit; 42 import java.util.concurrent.TimeUnit;
49 43
50 @TbCoreComponent 44 @TbCoreComponent
@@ -60,7 +54,7 @@ public class AlarmsCleanUpService { @@ -60,7 +54,7 @@ public class AlarmsCleanUpService {
60 private final AlarmDao alarmDao; 54 private final AlarmDao alarmDao;
61 private final AlarmService alarmService; 55 private final AlarmService alarmService;
62 private final RelationService relationService; 56 private final RelationService relationService;
63 - private final RuleEngineEntityActionService ruleEngineEntityActionService; 57 + private final EntityActionService entityActionService;
64 private final PartitionService partitionService; 58 private final PartitionService partitionService;
65 private final TbTenantProfileCache tenantProfileCache; 59 private final TbTenantProfileCache tenantProfileCache;
66 60
@@ -90,7 +84,7 @@ public class AlarmsCleanUpService { @@ -90,7 +84,7 @@ public class AlarmsCleanUpService {
90 toRemove.getData().forEach(alarmId -> { 84 toRemove.getData().forEach(alarmId -> {
91 relationService.deleteEntityRelations(tenantId, alarmId); 85 relationService.deleteEntityRelations(tenantId, alarmId);
92 Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm(); 86 Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm();
93 - ruleEngineEntityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null); 87 + entityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);
94 }); 88 });
95 89
96 totalRemoved += toRemove.getTotalElements(); 90 totalRemoved += toRemove.getTotalElements();
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.utils;
  17 +
  18 +import lombok.AccessLevel;
  19 +import lombok.NoArgsConstructor;
  20 +import org.apache.commons.csv.CSVFormat;
  21 +import org.apache.commons.csv.CSVRecord;
  22 +import org.apache.commons.io.input.CharSequenceReader;
  23 +
  24 +import java.util.List;
  25 +import java.util.stream.Collectors;
  26 +import java.util.stream.Stream;
  27 +
  28 +@NoArgsConstructor(access = AccessLevel.PRIVATE)
  29 +public class CsvUtils {
  30 +
  31 + public static List<List<String>> parseCsv(String content, Character delimiter) throws Exception {
  32 + CSVFormat csvFormat = delimiter.equals(',') ? CSVFormat.DEFAULT : CSVFormat.DEFAULT.withDelimiter(delimiter);
  33 +
  34 + List<CSVRecord> records;
  35 + try (CharSequenceReader reader = new CharSequenceReader(content)) {
  36 + records = csvFormat.parse(reader).getRecords();
  37 + }
  38 +
  39 + return records.stream()
  40 + .map(record -> Stream.iterate(0, i -> i < record.size(), i -> i + 1)
  41 + .map(record::get)
  42 + .collect(Collectors.toList()))
  43 + .collect(Collectors.toList());
  44 + }
  45 +
  46 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.utils;
  17 +
  18 +import org.apache.commons.lang3.math.NumberUtils;
  19 +import org.thingsboard.server.common.data.kv.DataType;
  20 +
  21 +import java.math.BigDecimal;
  22 +import java.util.Map;
  23 +
  24 +public class TypeCastUtil {
  25 +
  26 + private TypeCastUtil() {}
  27 +
  28 + public static Map.Entry<DataType, Object> castValue(String value) {
  29 + if (isNumber(value)) {
  30 + String formattedValue = value.replace(',', '.');
  31 + try {
  32 + BigDecimal bd = new BigDecimal(formattedValue);
  33 + if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
  34 + if (bd.scale() <= 16) {
  35 + return Map.entry(DataType.DOUBLE, bd.doubleValue());
  36 + }
  37 + } else {
  38 + return Map.entry(DataType.LONG, bd.longValueExact());
  39 + }
  40 + } catch (RuntimeException ignored) {}
  41 + } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
  42 + return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value));
  43 + }
  44 + return Map.entry(DataType.STRING, value);
  45 + }
  46 +
  47 + private static boolean isNumber(String value) {
  48 + return NumberUtils.isNumber(value.replace(',', '.'));
  49 + }
  50 +
  51 + private static boolean isSimpleDouble(String valueAsString) {
  52 + return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e");
  53 + }
  54 +
  55 +}
@@ -29,5 +29,7 @@ public interface DeviceCredentialsService { @@ -29,5 +29,7 @@ public interface DeviceCredentialsService {
29 29
30 DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); 30 DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
31 31
  32 + void formatCredentials(DeviceCredentials deviceCredentials);
  33 +
32 void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); 34 void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
33 } 35 }
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
29 29
30 import java.io.ByteArrayInputStream; 30 import java.io.ByteArrayInputStream;
31 import java.io.IOException; 31 import java.io.IOException;
  32 +import java.util.Optional;
32 33
33 @EqualsAndHashCode(callSuper = true) 34 @EqualsAndHashCode(callSuper = true)
34 @Slf4j 35 @Slf4j
@@ -83,6 +84,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen @@ -83,6 +84,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
83 this.setDeviceData(device.getDeviceData()); 84 this.setDeviceData(device.getDeviceData());
84 this.setFirmwareId(device.getFirmwareId()); 85 this.setFirmwareId(device.getFirmwareId());
85 this.setSoftwareId(device.getSoftwareId()); 86 this.setSoftwareId(device.getSoftwareId());
  87 + Optional.ofNullable(device.getAdditionalInfo()).ifPresent(this::setAdditionalInfo);
86 return this; 88 return this;
87 } 89 }
88 90
@@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.id.CustomerId; @@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
25 import org.thingsboard.server.common.data.id.TenantId; 25 import org.thingsboard.server.common.data.id.TenantId;
26 import org.thingsboard.server.common.data.validation.NoXss; 26 import org.thingsboard.server.common.data.validation.NoXss;
27 27
  28 +import java.util.Optional;
  29 +
28 @EqualsAndHashCode(callSuper = true) 30 @EqualsAndHashCode(callSuper = true)
29 public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId { 31 public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
30 32
@@ -56,6 +58,15 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements @@ -56,6 +58,15 @@ public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements
56 this.label = asset.getLabel(); 58 this.label = asset.getLabel();
57 } 59 }
58 60
  61 + public void update(Asset asset) {
  62 + this.tenantId = asset.getTenantId();
  63 + this.customerId = asset.getCustomerId();
  64 + this.name = asset.getName();
  65 + this.type = asset.getType();
  66 + this.label = asset.getLabel();
  67 + Optional.ofNullable(asset.getAdditionalInfo()).ifPresent(this::setAdditionalInfo);
  68 + }
  69 +
59 public TenantId getTenantId() { 70 public TenantId getTenantId() {
60 return tenantId; 71 return tenantId;
61 } 72 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import lombok.Getter;
  20 +import lombok.Setter;
  21 +import lombok.SneakyThrows;
  22 +import org.apache.commons.codec.binary.Hex;
  23 +
  24 +@Getter
  25 +@Setter
  26 +public abstract class AbstractLwM2MServerCredentialsWithKeys implements LwM2MServerCredentials {
  27 +
  28 + private String clientPublicKeyOrId;
  29 + private String clientSecretKey;
  30 +
  31 + @JsonIgnore
  32 + public byte[] getDecodedClientPublicKeyOrId() {
  33 + return getDecoded(clientPublicKeyOrId);
  34 + }
  35 +
  36 + @JsonIgnore
  37 + public byte[] getDecodedClientSecretKey() {
  38 + return getDecoded(clientSecretKey);
  39 + }
  40 +
  41 + @SneakyThrows
  42 + private static byte[] getDecoded(String key) {
  43 + return Hex.decodeHex(key.toLowerCase().toCharArray());
  44 + }
  45 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +import lombok.Getter;
  19 +import lombok.Setter;
  20 +
  21 +@Getter
  22 +@Setter
  23 +public class LwM2MBootstrapCredentials {
  24 + private LwM2MServerCredentials bootstrapServer;
  25 + private LwM2MServerCredentials lwm2mServer;
  26 +}
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.common.data.device.credentials.lwm2m; 16 package org.thingsboard.server.common.data.device.credentials.lwm2m;
17 17
18 import com.fasterxml.jackson.annotation.JsonIgnore; 18 import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 import com.fasterxml.jackson.annotation.JsonSubTypes; 20 import com.fasterxml.jackson.annotation.JsonSubTypes;
20 import com.fasterxml.jackson.annotation.JsonTypeInfo; 21 import com.fasterxml.jackson.annotation.JsonTypeInfo;
21 22
@@ -26,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -26,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
26 @JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"), 27 @JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"),
27 @JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"), 28 @JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"),
28 @JsonSubTypes.Type(value = RPKClientCredentials.class, name = "RPK"), 29 @JsonSubTypes.Type(value = RPKClientCredentials.class, name = "RPK"),
29 - @JsonSubTypes.Type(value = X509ClientCredentials.class, name = "X509")}) 30 + @JsonSubTypes.Type(value = X509ClientCredentials.class, name = "X509")
  31 +})
  32 +@JsonIgnoreProperties(ignoreUnknown = true)
30 public interface LwM2MClientCredentials { 33 public interface LwM2MClientCredentials {
31 34
32 @JsonIgnore 35 @JsonIgnore
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +import lombok.Getter;
  19 +import lombok.Setter;
  20 +
  21 +@Getter
  22 +@Setter
  23 +public class LwM2MDeviceCredentials {
  24 + private LwM2MClientCredentials client;
  25 + private LwM2MBootstrapCredentials bootstrap;
  26 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  20 +import com.fasterxml.jackson.annotation.JsonSubTypes;
  21 +import com.fasterxml.jackson.annotation.JsonTypeInfo;
  22 +
  23 +@JsonTypeInfo(
  24 + use = JsonTypeInfo.Id.NAME,
  25 + property = "securityMode")
  26 +@JsonSubTypes({
  27 + @JsonSubTypes.Type(value = NoSecServerCredentials.class, name = "NO_SEC"),
  28 + @JsonSubTypes.Type(value = PSKServerCredentials.class, name = "PSK"),
  29 + @JsonSubTypes.Type(value = RPKServerCredentials.class, name = "RPK"),
  30 + @JsonSubTypes.Type(value = X509ServerCredentials.class, name = "X509")
  31 +})
  32 +@JsonIgnoreProperties(ignoreUnknown = true)
  33 +public interface LwM2MServerCredentials {
  34 +
  35 + @JsonIgnore
  36 + LwM2MSecurityMode getSecurityMode();
  37 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +public class NoSecServerCredentials implements LwM2MServerCredentials {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.NO_SEC;
  23 + }
  24 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +public class PSKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.PSK;
  23 + }
  24 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +public class RPKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.RPK;
  23 + }
  24 +}
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.device.credentials.lwm2m;
  17 +
  18 +public class X509ServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.X509;
  23 + }
  24 +}
@@ -15,7 +15,6 @@ @@ -15,7 +15,6 @@
15 */ 15 */
16 package org.thingsboard.server.common.data.edge; 16 package org.thingsboard.server.common.data.edge;
17 17
18 -import com.fasterxml.jackson.databind.JsonNode;  
19 import lombok.EqualsAndHashCode; 18 import lombok.EqualsAndHashCode;
20 import lombok.Getter; 19 import lombok.Getter;
21 import lombok.Setter; 20 import lombok.Setter;
@@ -70,6 +69,19 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H @@ -70,6 +69,19 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
70 this.cloudEndpoint = edge.getCloudEndpoint(); 69 this.cloudEndpoint = edge.getCloudEndpoint();
71 } 70 }
72 71
  72 + public void update(Edge edge) {
  73 + this.tenantId = edge.getTenantId();
  74 + this.customerId = edge.getCustomerId();
  75 + this.rootRuleChainId = edge.getRootRuleChainId();
  76 + this.type = edge.getType();
  77 + this.label = edge.getLabel();
  78 + this.name = edge.getName();
  79 + this.routingKey = edge.getRoutingKey();
  80 + this.secret = edge.getSecret();
  81 + this.edgeLicenseKey = edge.getEdgeLicenseKey();
  82 + this.cloudEndpoint = edge.getCloudEndpoint();
  83 + }
  84 +
73 @Override 85 @Override
74 public String getSearchText() { 86 public String getSearchText() {
75 return getName(); 87 return getName();
@@ -577,6 +577,14 @@ public class JsonConverter { @@ -577,6 +577,14 @@ public class JsonConverter {
577 return GSON.toJson(element); 577 return GSON.toJson(element);
578 } 578 }
579 579
  580 + public static JsonObject toJsonObject(Object o) {
  581 + return (JsonObject) GSON.toJsonTree(o);
  582 + }
  583 +
  584 + public static <T> T fromJson(JsonElement element, Class<T> type) {
  585 + return GSON.fromJson(element, type);
  586 + }
  587 +
580 public static void setTypeCastEnabled(boolean enabled) { 588 public static void setTypeCastEnabled(boolean enabled) {
581 isTypeCastEnabled = enabled; 589 isTypeCastEnabled = enabled;
582 } 590 }
@@ -118,4 +118,9 @@ public class JacksonUtil { @@ -118,4 +118,9 @@ public class JacksonUtil {
118 public static <T> JsonNode valueToTree(T value) { 118 public static <T> JsonNode valueToTree(T value) {
119 return OBJECT_MAPPER.valueToTree(value); 119 return OBJECT_MAPPER.valueToTree(value);
120 } 120 }
  121 +
  122 + public static <T> T treeToValue(JsonNode tree, Class<T> type) throws JsonProcessingException {
  123 + return OBJECT_MAPPER.treeToValue(tree, type);
  124 + }
  125 +
121 } 126 }
@@ -227,6 +227,10 @@ @@ -227,6 +227,10 @@
227 <groupId>org.elasticsearch.client</groupId> 227 <groupId>org.elasticsearch.client</groupId>
228 <artifactId>rest</artifactId> 228 <artifactId>rest</artifactId>
229 </dependency> 229 </dependency>
  230 + <dependency>
  231 + <groupId>org.eclipse.leshan</groupId>
  232 + <artifactId>leshan-core</artifactId>
  233 + </dependency>
230 </dependencies> 234 </dependencies>
231 <build> 235 <build>
232 <plugins> 236 <plugins>
@@ -16,20 +16,28 @@ @@ -16,20 +16,28 @@
16 package org.thingsboard.server.dao.device; 16 package org.thingsboard.server.dao.device;
17 17
18 18
19 -import com.fasterxml.jackson.databind.node.ObjectNode;  
20 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.codec.binary.Hex;
  21 +import org.eclipse.leshan.core.util.SecurityUtil;
21 import org.hibernate.exception.ConstraintViolationException; 22 import org.hibernate.exception.ConstraintViolationException;
22 import org.springframework.beans.factory.annotation.Autowired; 23 import org.springframework.beans.factory.annotation.Autowired;
23 import org.springframework.cache.annotation.CacheEvict; 24 import org.springframework.cache.annotation.CacheEvict;
24 import org.springframework.cache.annotation.Cacheable; 25 import org.springframework.cache.annotation.Cacheable;
25 import org.springframework.stereotype.Service; 26 import org.springframework.stereotype.Service;
26 -import org.springframework.util.StringUtils;  
27 import org.thingsboard.common.util.JacksonUtil; 27 import org.thingsboard.common.util.JacksonUtil;
28 import org.thingsboard.server.common.data.Device; 28 import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.StringUtils;
29 import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; 30 import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
  31 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapCredentials;
30 import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; 32 import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
  33 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
  34 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MServerCredentials;
31 import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials; 35 import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials;
  36 +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKServerCredentials;
  37 +import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials;
  38 +import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKServerCredentials;
32 import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials; 39 import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
  40 +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ServerCredentials;
33 import org.thingsboard.server.common.data.id.DeviceId; 41 import org.thingsboard.server.common.data.id.DeviceId;
34 import org.thingsboard.server.common.data.id.EntityId; 42 import org.thingsboard.server.common.data.id.EntityId;
35 import org.thingsboard.server.common.data.id.TenantId; 43 import org.thingsboard.server.common.data.id.TenantId;
@@ -37,6 +45,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -37,6 +45,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
37 import org.thingsboard.server.common.msg.EncryptionUtil; 45 import org.thingsboard.server.common.msg.EncryptionUtil;
38 import org.thingsboard.server.dao.entity.AbstractEntityService; 46 import org.thingsboard.server.dao.entity.AbstractEntityService;
39 import org.thingsboard.server.dao.exception.DataValidationException; 47 import org.thingsboard.server.dao.exception.DataValidationException;
  48 +import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
40 import org.thingsboard.server.dao.service.DataValidator; 49 import org.thingsboard.server.dao.service.DataValidator;
41 50
42 import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; 51 import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
@@ -83,17 +92,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -83,17 +92,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
83 if (deviceCredentials.getCredentialsType() == null) { 92 if (deviceCredentials.getCredentialsType() == null) {
84 throw new DataValidationException("Device credentials type should be specified"); 93 throw new DataValidationException("Device credentials type should be specified");
85 } 94 }
86 - switch (deviceCredentials.getCredentialsType()) {  
87 - case X509_CERTIFICATE:  
88 - formatCertData(deviceCredentials);  
89 - break;  
90 - case MQTT_BASIC:  
91 - formatSimpleMqttCredentials(deviceCredentials);  
92 - break;  
93 - case LWM2M_CREDENTIALS:  
94 - formatSimpleLwm2mCredentials(deviceCredentials);  
95 - break;  
96 - } 95 + formatCredentials(deviceCredentials);
97 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); 96 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
98 credentialsValidator.validate(deviceCredentials, id -> tenantId); 97 credentialsValidator.validate(deviceCredentials, id -> tenantId);
99 try { 98 try {
@@ -109,6 +108,21 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -109,6 +108,21 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
109 } 108 }
110 } 109 }
111 110
  111 + @Override
  112 + public void formatCredentials(DeviceCredentials deviceCredentials) {
  113 + switch (deviceCredentials.getCredentialsType()) {
  114 + case X509_CERTIFICATE:
  115 + formatCertData(deviceCredentials);
  116 + break;
  117 + case MQTT_BASIC:
  118 + formatSimpleMqttCredentials(deviceCredentials);
  119 + break;
  120 + case LWM2M_CREDENTIALS:
  121 + formatSimpleLwm2mCredentials(deviceCredentials);
  122 + break;
  123 + }
  124 + }
  125 +
112 private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { 126 private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
113 BasicMqttCredentials mqttCredentials; 127 BasicMqttCredentials mqttCredentials;
114 try { 128 try {
@@ -117,11 +131,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -117,11 +131,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
117 throw new IllegalArgumentException(); 131 throw new IllegalArgumentException();
118 } 132 }
119 } catch (IllegalArgumentException e) { 133 } catch (IllegalArgumentException e) {
120 - throw new DataValidationException("Invalid credentials body for simple mqtt credentials!"); 134 + throw new DeviceCredentialsValidationException("Invalid credentials body for simple mqtt credentials!");
121 } 135 }
  136 +
122 if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) { 137 if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
123 - throw new DataValidationException("Both mqtt client id and user name are empty!"); 138 + throw new DeviceCredentialsValidationException("Both mqtt client id and user name are empty!");
124 } 139 }
  140 + if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
  141 + throw new DeviceCredentialsValidationException("Password cannot be specified along with client id");
  142 + }
  143 +
125 if (StringUtils.isEmpty(mqttCredentials.getClientId())) { 144 if (StringUtils.isEmpty(mqttCredentials.getClientId())) {
126 deviceCredentials.setCredentialsId(mqttCredentials.getUserName()); 145 deviceCredentials.setCredentialsId(mqttCredentials.getUserName());
127 } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) { 146 } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) {
@@ -129,7 +148,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -129,7 +148,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
129 } else { 148 } else {
130 deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName())); 149 deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName()));
131 } 150 }
132 - if (!StringUtils.isEmpty(mqttCredentials.getPassword())) { 151 + if (StringUtils.isNotEmpty(mqttCredentials.getPassword())) {
133 mqttCredentials.setPassword(mqttCredentials.getPassword()); 152 mqttCredentials.setPassword(mqttCredentials.getPassword());
134 } 153 }
135 deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); 154 deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
@@ -143,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -143,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
143 } 162 }
144 163
145 private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) { 164 private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) {
146 - LwM2MClientCredentials clientCredentials;  
147 - ObjectNode json; 165 + LwM2MDeviceCredentials lwM2MCredentials;
148 try { 166 try {
149 - json = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), ObjectNode.class);  
150 - if (json == null) {  
151 - throw new IllegalArgumentException();  
152 - }  
153 - clientCredentials = JacksonUtil.convertValue(json.get("client"), LwM2MClientCredentials.class);  
154 - if (clientCredentials == null) {  
155 - throw new IllegalArgumentException();  
156 - } 167 + lwM2MCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), LwM2MDeviceCredentials.class);
  168 + validateLwM2MDeviceCredentials(lwM2MCredentials);
157 } catch (IllegalArgumentException e) { 169 } catch (IllegalArgumentException e) {
158 - throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); 170 + throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
159 } 171 }
160 172
161 String credentialsId = null; 173 String credentialsId = null;
  174 + LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
162 175
163 switch (clientCredentials.getSecurityConfigClientMode()) { 176 switch (clientCredentials.getSecurityConfigClientMode()) {
164 case NO_SEC: 177 case NO_SEC:
@@ -174,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -174,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
174 String cert = EncryptionUtil.trimNewLines(x509Config.getCert()); 187 String cert = EncryptionUtil.trimNewLines(x509Config.getCert());
175 String sha3Hash = EncryptionUtil.getSha3Hash(cert); 188 String sha3Hash = EncryptionUtil.getSha3Hash(cert);
176 x509Config.setCert(cert); 189 x509Config.setCert(cert);
177 - ((ObjectNode) json.get("client")).put("cert", cert);  
178 - deviceCredentials.setCredentialsValue(JacksonUtil.toString(json)); 190 + ((X509ClientCredentials) clientCredentials).setCert(cert);
  191 + deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials));
179 credentialsId = sha3Hash; 192 credentialsId = sha3Hash;
180 } else { 193 } else {
181 credentialsId = x509Config.getEndpoint(); 194 credentialsId = x509Config.getEndpoint();
@@ -183,11 +196,163 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -183,11 +196,163 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
183 break; 196 break;
184 } 197 }
185 if (credentialsId == null) { 198 if (credentialsId == null) {
186 - throw new DataValidationException("Invalid credentials body for LwM2M credentials!"); 199 + throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
187 } 200 }
188 deviceCredentials.setCredentialsId(credentialsId); 201 deviceCredentials.setCredentialsId(credentialsId);
189 } 202 }
190 203
  204 + private void validateLwM2MDeviceCredentials(LwM2MDeviceCredentials lwM2MCredentials) {
  205 + if (lwM2MCredentials == null) {
  206 + throw new DeviceCredentialsValidationException("LwM2M credentials should be specified!");
  207 + }
  208 +
  209 + LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
  210 + if (clientCredentials == null) {
  211 + throw new DeviceCredentialsValidationException("LwM2M client credentials should be specified!");
  212 + }
  213 + validateLwM2MClientCredentials(clientCredentials);
  214 +
  215 + LwM2MBootstrapCredentials bootstrapCredentials = lwM2MCredentials.getBootstrap();
  216 + if (bootstrapCredentials == null) {
  217 + throw new DeviceCredentialsValidationException("LwM2M bootstrap credentials should be specified!");
  218 + }
  219 +
  220 + LwM2MServerCredentials bootstrapServerCredentials = bootstrapCredentials.getBootstrapServer();
  221 + if (bootstrapServerCredentials == null) {
  222 + throw new DeviceCredentialsValidationException("LwM2M bootstrap server credentials should be specified!");
  223 + }
  224 + validateServerCredentials(bootstrapServerCredentials, "Bootstrap server");
  225 +
  226 + LwM2MServerCredentials lwm2mServerCredentials = bootstrapCredentials.getLwm2mServer();
  227 + if (lwm2mServerCredentials == null) {
  228 + throw new DeviceCredentialsValidationException("LwM2M lwm2m server credentials should be specified!");
  229 + }
  230 + validateServerCredentials(lwm2mServerCredentials, "LwM2M server");
  231 + }
  232 +
  233 + private void validateLwM2MClientCredentials(LwM2MClientCredentials clientCredentials) {
  234 + if (StringUtils.isEmpty(clientCredentials.getEndpoint())) {
  235 + throw new DeviceCredentialsValidationException("LwM2M client endpoint should be specified!");
  236 + }
  237 +
  238 + switch (clientCredentials.getSecurityConfigClientMode()) {
  239 + case NO_SEC:
  240 + break;
  241 + case PSK:
  242 + PSKClientCredentials pskCredentials = (PSKClientCredentials) clientCredentials;
  243 + if (StringUtils.isEmpty(pskCredentials.getIdentity())) {
  244 + throw new DeviceCredentialsValidationException("LwM2M client PSK identity should be specified!");
  245 + }
  246 +
  247 + String pskKey = pskCredentials.getKey();
  248 + if (StringUtils.isEmpty(pskKey)) {
  249 + throw new DeviceCredentialsValidationException("LwM2M client PSK key should be specified!");
  250 + }
  251 +
  252 + if (!pskKey.matches("-?[0-9a-fA-F]+")) {
  253 + throw new DeviceCredentialsValidationException("LwM2M client PSK key should be HexDecimal format!");
  254 + }
  255 +
  256 + if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
  257 + throw new DeviceCredentialsValidationException("LwM2M client PSK key must be 32, 64, 128 characters!");
  258 + }
  259 + break;
  260 + case RPK:
  261 + RPKClientCredentials rpkCredentials = (RPKClientCredentials) clientCredentials;
  262 +
  263 + if (StringUtils.isEmpty(rpkCredentials.getKey())) {
  264 + throw new DeviceCredentialsValidationException("LwM2M client RPK key should be specified!");
  265 + }
  266 +
  267 + try {
  268 + SecurityUtil.publicKey.decode(rpkCredentials.getDecodedKey());
  269 + } catch (Exception e) {
  270 + throw new DeviceCredentialsValidationException("LwM2M client RPK key should be in RFC7250 standard!");
  271 + }
  272 + break;
  273 + case X509:
  274 + X509ClientCredentials x509CCredentials = (X509ClientCredentials) clientCredentials;
  275 + if (x509CCredentials.getCert() != null) {
  276 + try {
  277 + SecurityUtil.certificate.decode(Hex.decodeHex(x509CCredentials.getCert().toLowerCase().toCharArray()));
  278 + } catch (Exception e) {
  279 + throw new DeviceCredentialsValidationException("LwM2M client X509 certificate should be in DER-encoded X.509 format!");
  280 + }
  281 + }
  282 + break;
  283 + }
  284 + }
  285 +
  286 + private void validateServerCredentials(LwM2MServerCredentials serverCredentials, String server) {
  287 + switch (serverCredentials.getSecurityMode()) {
  288 + case NO_SEC:
  289 + break;
  290 + case PSK:
  291 + PSKServerCredentials pskCredentials = (PSKServerCredentials) serverCredentials;
  292 + if (StringUtils.isEmpty(pskCredentials.getClientPublicKeyOrId())) {
  293 + throw new DeviceCredentialsValidationException(server + " client PSK public key or id should be specified!");
  294 + }
  295 +
  296 + String pskKey = pskCredentials.getClientSecretKey();
  297 + if (StringUtils.isEmpty(pskKey)) {
  298 + throw new DeviceCredentialsValidationException(server + " client PSK key should be specified!");
  299 + }
  300 +
  301 + if (!pskKey.matches("-?[0-9a-fA-F]+")) {
  302 + throw new DeviceCredentialsValidationException(server + " client PSK key should be HexDecimal format!");
  303 + }
  304 +
  305 + if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
  306 + throw new DeviceCredentialsValidationException(server + " client PSK key must be 32, 64, 128 characters!");
  307 + }
  308 + break;
  309 + case RPK:
  310 + RPKServerCredentials rpkCredentials = (RPKServerCredentials) serverCredentials;
  311 +
  312 + if (StringUtils.isEmpty(rpkCredentials.getClientPublicKeyOrId())) {
  313 + throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be specified!");
  314 + }
  315 +
  316 + try {
  317 + SecurityUtil.publicKey.decode(rpkCredentials.getDecodedClientPublicKeyOrId());
  318 + } catch (Exception e) {
  319 + throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be in RFC7250 standard!");
  320 + }
  321 +
  322 + if (StringUtils.isEmpty(rpkCredentials.getClientSecretKey())) {
  323 + throw new DeviceCredentialsValidationException(server + " client RPK secret key should be specified!");
  324 + }
  325 +
  326 + try {
  327 + SecurityUtil.privateKey.decode(rpkCredentials.getDecodedClientSecretKey());
  328 + } catch (Exception e) {
  329 + throw new DeviceCredentialsValidationException(server + " client RPK secret key should be in RFC5958 standard!");
  330 + }
  331 + break;
  332 + case X509:
  333 + X509ServerCredentials x509CCredentials = (X509ServerCredentials) serverCredentials;
  334 + if (StringUtils.isEmpty(x509CCredentials.getClientPublicKeyOrId())) {
  335 + throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be specified!");
  336 + }
  337 +
  338 + try {
  339 + SecurityUtil.certificate.decode(x509CCredentials.getDecodedClientPublicKeyOrId());
  340 + } catch (Exception e) {
  341 + throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be in DER-encoded X.509 format!");
  342 + }
  343 + if (StringUtils.isEmpty(x509CCredentials.getClientSecretKey())) {
  344 + throw new DeviceCredentialsValidationException(server + " client X509 secret key should be specified!");
  345 + }
  346 +
  347 + try {
  348 + SecurityUtil.privateKey.decode(x509CCredentials.getDecodedClientSecretKey());
  349 + } catch (Exception e) {
  350 + throw new DeviceCredentialsValidationException(server + " client X509 secret key should be in RFC5958 standard!");
  351 + }
  352 + break;
  353 + }
  354 + }
  355 +
191 @Override 356 @Override
192 @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId") 357 @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
193 public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { 358 public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
@@ -201,38 +366,38 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen @@ -201,38 +366,38 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
201 @Override 366 @Override
202 protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) { 367 protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) {
203 if (deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()) != null) { 368 if (deviceCredentialsDao.findByDeviceId(tenantId, deviceCredentials.getDeviceId().getId()) != null) {
204 - throw new DataValidationException("Credentials for this device are already specified!"); 369 + throw new DeviceCredentialsValidationException("Credentials for this device are already specified!");
205 } 370 }
206 if (deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()) != null) { 371 if (deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()) != null) {
207 - throw new DataValidationException("Device credentials are already assigned to another device!"); 372 + throw new DeviceCredentialsValidationException("Device credentials are already assigned to another device!");
208 } 373 }
209 } 374 }
210 375
211 @Override 376 @Override
212 protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { 377 protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) {
213 if (deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()) == null) { 378 if (deviceCredentialsDao.findById(tenantId, deviceCredentials.getUuidId()) == null) {
214 - throw new DataValidationException("Unable to update non-existent device credentials!"); 379 + throw new DeviceCredentialsValidationException("Unable to update non-existent device credentials!");
215 } 380 }
216 DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId()); 381 DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
217 if (existingCredentials != null && !existingCredentials.getId().equals(deviceCredentials.getId())) { 382 if (existingCredentials != null && !existingCredentials.getId().equals(deviceCredentials.getId())) {
218 - throw new DataValidationException("Device credentials are already assigned to another device!"); 383 + throw new DeviceCredentialsValidationException("Device credentials are already assigned to another device!");
219 } 384 }
220 } 385 }
221 386
222 @Override 387 @Override
223 protected void validateDataImpl(TenantId tenantId, DeviceCredentials deviceCredentials) { 388 protected void validateDataImpl(TenantId tenantId, DeviceCredentials deviceCredentials) {
224 if (deviceCredentials.getDeviceId() == null) { 389 if (deviceCredentials.getDeviceId() == null) {
225 - throw new DataValidationException("Device credentials should be assigned to device!"); 390 + throw new DeviceCredentialsValidationException("Device credentials should be assigned to device!");
226 } 391 }
227 if (deviceCredentials.getCredentialsType() == null) { 392 if (deviceCredentials.getCredentialsType() == null) {
228 - throw new DataValidationException("Device credentials type should be specified!"); 393 + throw new DeviceCredentialsValidationException("Device credentials type should be specified!");
229 } 394 }
230 if (StringUtils.isEmpty(deviceCredentials.getCredentialsId())) { 395 if (StringUtils.isEmpty(deviceCredentials.getCredentialsId())) {
231 - throw new DataValidationException("Device credentials id should be specified!"); 396 + throw new DeviceCredentialsValidationException("Device credentials id should be specified!");
232 } 397 }
233 Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId()); 398 Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId());
234 if (device == null) { 399 if (device == null) {
235 - throw new DataValidationException("Can't assign device credentials to non-existent device!"); 400 + throw new DeviceCredentialsValidationException("Can't assign device credentials to non-existent device!");
236 } 401 }
237 } 402 }
238 }; 403 };
@@ -228,6 +228,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -228,6 +228,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
228 if (foundDeviceCredentials == null) { 228 if (foundDeviceCredentials == null) {
229 deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); 229 deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
230 } else { 230 } else {
  231 + deviceCredentials.setId(foundDeviceCredentials.getId());
231 deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); 232 deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials);
232 } 233 }
233 } 234 }
@@ -241,7 +242,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @@ -241,7 +242,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
241 deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); 242 deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
242 deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); 243 deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
243 deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20)); 244 deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20));
244 - deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); 245 + deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
245 } 246 }
246 return savedDevice; 247 return savedDevice;
247 } 248 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.exception;
  17 +
  18 +public class DeviceCredentialsValidationException extends DataValidationException {
  19 + public DeviceCredentialsValidationException(String message) {
  20 + super(message);
  21 + }
  22 +}
@@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link'; @@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 import { EntitySubtype } from '@app/shared/models/entity-type.models'; 23 import { EntitySubtype } from '@app/shared/models/entity-type.models';
24 import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models'; 24 import { Asset, AssetInfo, AssetSearchQuery } from '@app/shared/models/asset.models';
  25 +import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
25 26
26 @Injectable({ 27 @Injectable({
27 providedIn: 'root' 28 providedIn: 'root'
@@ -105,4 +106,8 @@ export class AssetService { @@ -105,4 +106,8 @@ export class AssetService {
105 defaultHttpOptionsFromConfig(config)); 106 defaultHttpOptionsFromConfig(config));
106 } 107 }
107 108
  109 + public bulkImportAssets(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
  110 + return this.http.post<BulkImportResult>('/api/asset/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
  111 + }
  112 +
108 } 113 }
@@ -30,6 +30,7 @@ import { @@ -30,6 +30,7 @@ import {
30 } from '@app/shared/models/device.models'; 30 } from '@app/shared/models/device.models';
31 import { EntitySubtype } from '@app/shared/models/entity-type.models'; 31 import { EntitySubtype } from '@app/shared/models/entity-type.models';
32 import { AuthService } from '@core/auth/auth.service'; 32 import { AuthService } from '@core/auth/auth.service';
  33 +import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
33 import { PersistentRpc } from '@shared/models/rpc.models'; 34 import { PersistentRpc } from '@shared/models/rpc.models';
34 35
35 @Injectable({ 36 @Injectable({
@@ -178,4 +179,8 @@ export class DeviceService { @@ -178,4 +179,8 @@ export class DeviceService {
178 defaultHttpOptionsFromConfig(config)); 179 defaultHttpOptionsFromConfig(config));
179 } 180 }
180 181
  182 + public bulkImportDevices(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
  183 + return this.http.post<BulkImportResult>('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
  184 + }
  185 +
181 } 186 }
@@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data'; @@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
23 import { EntitySubtype } from '@app/shared/models/entity-type.models'; 23 import { EntitySubtype } from '@app/shared/models/entity-type.models';
24 import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models'; 24 import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models';
25 import { EntityId } from '@shared/models/id/entity-id'; 25 import { EntityId } from '@shared/models/id/entity-id';
  26 +import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
26 27
27 @Injectable({ 28 @Injectable({
28 providedIn: 'root' 29 providedIn: 'root'
@@ -59,7 +60,7 @@ export class EdgeService { @@ -59,7 +60,7 @@ export class EdgeService {
59 } 60 }
60 61
61 public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '', 62 public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '',
62 - config?: RequestConfig): Observable<PageData<EdgeInfo>> { 63 + config?: RequestConfig): Observable<PageData<EdgeInfo>> {
63 return this.http.get<PageData<EdgeInfo>>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`, 64 return this.http.get<PageData<EdgeInfo>>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`,
64 defaultHttpOptionsFromConfig(config)); 65 defaultHttpOptionsFromConfig(config));
65 } 66 }
@@ -108,4 +109,8 @@ export class EdgeService { @@ -108,4 +109,8 @@ export class EdgeService {
108 public findByName(edgeName: string, config?: RequestConfig): Observable<Edge> { 109 public findByName(edgeName: string, config?: RequestConfig): Observable<Edge> {
109 return this.http.get<Edge>(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config)); 110 return this.http.get<Edge>(`/api/tenant/edges?edgeName=${edgeName}`, defaultHttpOptionsFromConfig(config));
110 } 111 }
  112 +
  113 + public bulkImportEdges(entitiesData: BulkImportRequest, config?: RequestConfig): Observable<BulkImportResult> {
  114 + return this.http.post<BulkImportResult>('/api/edge/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config));
  115 + }
111 } 116 }
@@ -59,7 +59,7 @@ import { @@ -59,7 +59,7 @@ import {
59 ImportEntityData 59 ImportEntityData
60 } from '@shared/models/entity.models'; 60 } from '@shared/models/entity.models';
61 import { EntityRelationService } from '@core/http/entity-relation.service'; 61 import { EntityRelationService } from '@core/http/entity-relation.service';
62 -import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull } from '@core/utils'; 62 +import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
63 import { Asset } from '@shared/models/asset.models'; 63 import { Asset } from '@shared/models/asset.models';
64 import { Device, DeviceCredentialsType } from '@shared/models/device.models'; 64 import { Device, DeviceCredentialsType } from '@shared/models/device.models';
65 import { AttributeService } from '@core/http/attribute.service'; 65 import { AttributeService } from '@core/http/attribute.service';
@@ -964,7 +964,12 @@ export class EntityService { @@ -964,7 +964,12 @@ export class EntityService {
964 map(() => { 964 map(() => {
965 return { create: { entity: 1 } } as ImportEntitiesResultInfo; 965 return { create: { entity: 1 } } as ImportEntitiesResultInfo;
966 }), 966 }),
967 - catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) 967 + catchError(err => of({
  968 + error: {
  969 + entity: 1,
  970 + errors: err.message
  971 + }
  972 + } as ImportEntitiesResultInfo))
968 ); 973 );
969 }), 974 }),
970 catchError(err => { 975 catchError(err => {
@@ -988,13 +993,28 @@ export class EntityService { @@ -988,13 +993,28 @@ export class EntityService {
988 map(() => { 993 map(() => {
989 return { update: { entity: 1 } } as ImportEntitiesResultInfo; 994 return { update: { entity: 1 } } as ImportEntitiesResultInfo;
990 }), 995 }),
991 - catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) 996 + catchError(updateError => of({
  997 + error: {
  998 + entity: 1,
  999 + errors: updateError.message
  1000 + }
  1001 + } as ImportEntitiesResultInfo))
992 ); 1002 );
993 }), 1003 }),
994 - catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo)) 1004 + catchError(findErr => of({
  1005 + error: {
  1006 + entity: 1,
  1007 + errors: `Line: ${entityData.lineNumber}; Error: ${findErr.error.message}`
  1008 + }
  1009 + } as ImportEntitiesResultInfo))
995 ); 1010 );
996 } else { 1011 } else {
997 - return of({ error: { entity: 1 } } as ImportEntitiesResultInfo); 1012 + return of({
  1013 + error: {
  1014 + entity: 1,
  1015 + errors: `Line: ${entityData.lineNumber}; Error: ${err.error.message}`
  1016 + }
  1017 + } as ImportEntitiesResultInfo);
998 } 1018 }
999 }) 1019 })
1000 ); 1020 );
@@ -1050,7 +1070,6 @@ export class EntityService { @@ -1050,7 +1070,6 @@ export class EntityService {
1050 break; 1070 break;
1051 } 1071 }
1052 return saveEntityObservable; 1072 return saveEntityObservable;
1053 -  
1054 } 1073 }
1055 1074
1056 private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData, 1075 private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData,
@@ -1123,15 +1142,31 @@ export class EntityService { @@ -1123,15 +1142,31 @@ export class EntityService {
1123 public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> { 1142 public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> {
1124 const observables: Observable<string>[] = []; 1143 const observables: Observable<string>[] = [];
1125 let observable: Observable<string>; 1144 let observable: Observable<string>;
1126 - if (entityData.accessToken && entityData.accessToken !== '') { 1145 + if (Object.keys(entityData.credential).length) {
  1146 + let credentialsType: DeviceCredentialsType;
  1147 + let credentialsId: string = null;
  1148 + let credentialsValue: string = null;
  1149 + if (isDefinedAndNotNull(entityData.credential.mqtt)) {
  1150 + credentialsType = DeviceCredentialsType.MQTT_BASIC;
  1151 + credentialsValue = JSON.stringify(entityData.credential.mqtt);
  1152 + } else if (isDefinedAndNotNull(entityData.credential.lwm2m)) {
  1153 + credentialsType = DeviceCredentialsType.LWM2M_CREDENTIALS;
  1154 + credentialsValue = JSON.stringify(entityData.credential.lwm2m);
  1155 + } else if (isNotEmptyStr(entityData.credential.x509)) {
  1156 + credentialsType = DeviceCredentialsType.X509_CERTIFICATE;
  1157 + credentialsValue = entityData.credential.x509;
  1158 + } else {
  1159 + credentialsType = DeviceCredentialsType.ACCESS_TOKEN;
  1160 + credentialsId = entityData.credential.accessToken;
  1161 + }
1127 observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe( 1162 observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe(
1128 mergeMap((credentials) => { 1163 mergeMap((credentials) => {
1129 - credentials.credentialsId = entityData.accessToken;  
1130 - credentials.credentialsType = DeviceCredentialsType.ACCESS_TOKEN;  
1131 - credentials.credentialsValue = null; 1164 + credentials.credentialsId = credentialsId;
  1165 + credentials.credentialsType = credentialsType;
  1166 + credentials.credentialsValue = credentialsValue;
1132 return this.deviceService.saveDeviceCredentials(credentials, config).pipe( 1167 return this.deviceService.saveDeviceCredentials(credentials, config).pipe(
1133 map(() => 'ok'), 1168 map(() => 'ok'),
1134 - catchError(err => of('error')) 1169 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1135 ); 1170 );
1136 }) 1171 })
1137 ); 1172 );
@@ -1141,7 +1176,7 @@ export class EntityService { @@ -1141,7 +1176,7 @@ export class EntityService {
1141 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, 1176 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
1142 entityData.attributes.shared, config).pipe( 1177 entityData.attributes.shared, config).pipe(
1143 map(() => 'ok'), 1178 map(() => 'ok'),
1144 - catchError(err => of('error')) 1179 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1145 ); 1180 );
1146 observables.push(observable); 1181 observables.push(observable);
1147 } 1182 }
@@ -1149,23 +1184,23 @@ export class EntityService { @@ -1149,23 +1184,23 @@ export class EntityService {
1149 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, 1184 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE,
1150 entityData.attributes.server, config).pipe( 1185 entityData.attributes.server, config).pipe(
1151 map(() => 'ok'), 1186 map(() => 'ok'),
1152 - catchError(err => of('error')) 1187 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1153 ); 1188 );
1154 observables.push(observable); 1189 observables.push(observable);
1155 } 1190 }
1156 if (entityData.timeseries && entityData.timeseries.length) { 1191 if (entityData.timeseries && entityData.timeseries.length) {
1157 observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe( 1192 observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe(
1158 map(() => 'ok'), 1193 map(() => 'ok'),
1159 - catchError(err => of('error')) 1194 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1160 ); 1195 );
1161 observables.push(observable); 1196 observables.push(observable);
1162 } 1197 }
1163 if (observables.length) { 1198 if (observables.length) {
1164 return forkJoin(observables).pipe( 1199 return forkJoin(observables).pipe(
1165 map((response) => { 1200 map((response) => {
1166 - const hasError = response.filter((status) => status === 'error').length > 0;  
1167 - if (hasError) {  
1168 - throw Error(); 1201 + const hasError = response.filter((status) => status !== 'ok');
  1202 + if (hasError.length > 0) {
  1203 + throw Error(hasError.join('\n'));
1169 } else { 1204 } else {
1170 return response; 1205 return response;
1171 } 1206 }
@@ -94,7 +94,7 @@ @@ -94,7 +94,7 @@
94 <mat-step [stepControl]="columnTypesFormGroup"> 94 <mat-step [stepControl]="columnTypesFormGroup">
95 <form [formGroup]="columnTypesFormGroup"> 95 <form [formGroup]="columnTypesFormGroup">
96 <ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template> 96 <ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template>
97 - <tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment> 97 + <tb-table-columns-assignment #columnsAssignmentComponent formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment>
98 </form> 98 </form>
99 <div fxLayout="row wrap" fxLayoutAlign="space-between center"> 99 <div fxLayout="row wrap" fxLayoutAlign="space-between center">
100 <button mat-button 100 <button mat-button
@@ -113,21 +113,32 @@ @@ -113,21 +113,32 @@
113 </mat-step> 113 </mat-step>
114 <mat-step> 114 <mat-step>
115 <ng-template matStepLabel>{{ 'import.stepper-text.creat-entities' | translate }}</ng-template> 115 <ng-template matStepLabel>{{ 'import.stepper-text.creat-entities' | translate }}</ng-template>
116 - <mat-progress-bar color="warn" class="tb-import-progress" mode="determinate" [value]="progressCreate"> 116 + <mat-progress-bar color="warn" class="tb-import-progress" mode="indeterminate">
117 </mat-progress-bar> 117 </mat-progress-bar>
118 </mat-step> 118 </mat-step>
119 <mat-step> 119 <mat-step>
120 <ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template> 120 <ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template>
121 <div fxLayout="column"> 121 <div fxLayout="column">
122 - <p class="mat-body-1" *ngIf="this.statistical?.create && this.statistical?.create.entity">  
123 - {{ translate.instant('import.message.create-entities', {count: this.statistical.create.entity}) }} 122 + <p class="mat-body-1" *ngIf="this.statistical?.created">
  123 + {{ translate.instant('import.message.create-entities', {count: this.statistical.created}) }}
124 </p> 124 </p>
125 - <p class="mat-body-1" *ngIf="this.statistical?.update && this.statistical?.update.entity">  
126 - {{ translate.instant('import.message.update-entities', {count: this.statistical.update.entity}) }} 125 + <p class="mat-body-1" *ngIf="this.statistical?.updated">
  126 + {{ translate.instant('import.message.update-entities', {count: this.statistical.updated}) }}
127 </p> 127 </p>
128 - <p class="mat-body-1" *ngIf="this.statistical?.error && this.statistical?.error.entity">  
129 - {{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }} 128 + <p class="mat-body-1" style="margin-bottom: 0.8em" *ngIf="this.statistical?.errors">
  129 + {{ translate.instant('import.message.error-entities', {count: this.statistical.errors}) }}
130 </p> 130 </p>
  131 + <mat-expansion-panel class="advanced-logs" [expanded]="false"
  132 + *ngIf="this.statistical?.errorsList?.length"
  133 + (opened)="initEditor()">
  134 + <mat-expansion-panel-header [collapsedHeight]="'38px'" [expandedHeight]="'38px'">
  135 + <mat-panel-title>
  136 + <div class="tb-small" translate>import.details</div>
  137 + </mat-panel-title>
  138 + </mat-expansion-panel-header>
  139 + <mat-divider></mat-divider>
  140 + <div #failureDetailsEditor class="tb-failure-details"></div>
  141 + </mat-expansion-panel>
131 </div> 142 </div>
132 <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px"> 143 <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
133 <button mat-raised-button 144 <button mat-raised-button
@@ -26,5 +26,30 @@ @@ -26,5 +26,30 @@
26 .tb-import-progress{ 26 .tb-import-progress{
27 margin: 7px 0; 27 margin: 7px 0;
28 } 28 }
  29 +
  30 + .tb-failure-details {
  31 + width: 100%;
  32 + min-width: 300px;
  33 + height: 100%;
  34 + min-height: 50px;
  35 + margin-top: 8px;
  36 + }
  37 +
  38 + .mat-expansion-panel {
  39 + box-shadow: none;
  40 + &.advanced-logs {
  41 + border: 1px groove rgba(0, 0, 0, .25);
  42 + padding: 0;
  43 + margin-bottom: 1.6em;
  44 +
  45 + .mat-expansion-panel-header {
  46 + padding: 0 8px;
  47 + }
  48 +
  49 + .mat-expansion-panel-body {
  50 + padding: 0;
  51 + }
  52 + }
  53 + }
29 } 54 }
30 } 55 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, Inject, OnInit, ViewChild } from '@angular/core'; 17 +import { AfterViewInit, Component, ElementRef, Inject, Renderer2, ViewChild } from '@angular/core';
18 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 18 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
@@ -26,14 +26,20 @@ import { TranslateService } from '@ngx-translate/core'; @@ -26,14 +26,20 @@ import { TranslateService } from '@ngx-translate/core';
26 import { ActionNotificationShow } from '@core/notification/notification.actions'; 26 import { ActionNotificationShow } from '@core/notification/notification.actions';
27 import { MatVerticalStepper } from '@angular/material/stepper'; 27 import { MatVerticalStepper } from '@angular/material/stepper';
28 import { 28 import {
  29 + BulkImportRequest,
  30 + BulkImportResult,
  31 + ColumnMapping,
29 convertCSVToJson, 32 convertCSVToJson,
30 CsvColumnParam, 33 CsvColumnParam,
  34 + CSVDelimiter,
31 CsvToJsonConfig, 35 CsvToJsonConfig,
32 CsvToJsonResult, 36 CsvToJsonResult,
33 ImportEntityColumnType 37 ImportEntityColumnType
34 } from '@home/components/import-export/import-export.models'; 38 } from '@home/components/import-export/import-export.models';
35 -import { EdgeImportEntityData, ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models';  
36 import { ImportExportService } from '@home/components/import-export/import-export.service'; 39 import { ImportExportService } from '@home/components/import-export/import-export.service';
  40 +import { TableColumnsAssignmentComponent } from '@home/components/import-export/table-columns-assignment.component';
  41 +import { Ace } from 'ace-builds';
  42 +import { getAce } from '@shared/models/ace/ace.models';
37 43
38 export interface ImportDialogCsvData { 44 export interface ImportDialogCsvData {
39 entityType: EntityType; 45 entityType: EntityType;
@@ -48,15 +54,21 @@ export interface ImportDialogCsvData { @@ -48,15 +54,21 @@ export interface ImportDialogCsvData {
48 styleUrls: ['./import-dialog-csv.component.scss'] 54 styleUrls: ['./import-dialog-csv.component.scss']
49 }) 55 })
50 export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean> 56 export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean>
51 - implements OnInit { 57 + implements AfterViewInit {
52 58
53 @ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper; 59 @ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper;
54 60
  61 + @ViewChild('columnsAssignmentComponent', {static: true})
  62 + columnsAssignmentComponent: TableColumnsAssignmentComponent;
  63 +
  64 + @ViewChild('failureDetailsEditor')
  65 + failureDetailsEditorElmRef: ElementRef;
  66 +
55 entityType: EntityType; 67 entityType: EntityType;
56 importTitle: string; 68 importTitle: string;
57 importFileLabel: string; 69 importFileLabel: string;
58 70
59 - delimiters: {key: string, value: string}[] = [{ 71 + delimiters: { key: CSVDelimiter, value: string }[] = [{
60 key: ',', 72 key: ',',
61 value: ',' 73 value: ','
62 }, { 74 }, {
@@ -77,9 +89,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -77,9 +89,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
77 columnTypesFormGroup: FormGroup; 89 columnTypesFormGroup: FormGroup;
78 90
79 isImportData = false; 91 isImportData = false;
80 - progressCreate = 0;  
81 - statistical: ImportEntitiesResultInfo; 92 + statistical: BulkImportResult;
82 93
  94 + private allowAssignColumn: ImportEntityColumnType[];
  95 + private initEditorComponent = false;
83 private parseData: CsvToJsonResult; 96 private parseData: CsvToJsonResult;
84 97
85 constructor(protected store: Store<AppState>, 98 constructor(protected store: Store<AppState>,
@@ -88,7 +101,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -88,7 +101,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
88 public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>, 101 public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>,
89 public translate: TranslateService, 102 public translate: TranslateService,
90 private importExport: ImportExportService, 103 private importExport: ImportExportService,
91 - private fb: FormBuilder) { 104 + private fb: FormBuilder,
  105 + private renderer: Renderer2) {
92 super(store, router, dialogRef); 106 super(store, router, dialogRef);
93 this.entityType = data.entityType; 107 this.entityType = data.entityType;
94 this.importTitle = data.importTitle; 108 this.importTitle = data.importTitle;
@@ -109,7 +123,12 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -109,7 +123,12 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
109 }); 123 });
110 } 124 }
111 125
112 - ngOnInit(): void { 126 + ngAfterViewInit() {
  127 + let columns = this.columnsAssignmentComponent.columnTypes;
  128 + if (this.entityType === EntityType.DEVICE) {
  129 + columns = columns.concat(this.columnsAssignmentComponent.columnDeviceCredentials);
  130 + }
  131 + this.allowAssignColumn = columns.map(column => column.value);
113 } 132 }
114 133
115 cancel(): void { 134 cancel(): void {
@@ -157,8 +176,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -157,8 +176,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
157 return convertCSVToJson(importData, config, 176 return convertCSVToJson(importData, config,
158 (messageId, params) => { 177 (messageId, params) => {
159 this.store.dispatch(new ActionNotificationShow( 178 this.store.dispatch(new ActionNotificationShow(
160 - {message: this.translate.instant(messageId, params),  
161 - type: 'error'})); 179 + {
  180 + message: this.translate.instant(messageId, params),
  181 + type: 'error'
  182 + }));
162 } 183 }
163 ); 184 );
164 } 185 }
@@ -168,9 +189,14 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -168,9 +189,14 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
168 const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value; 189 const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value;
169 for (let i = 0; i < this.parseData.headers.length; i++) { 190 for (let i = 0; i < this.parseData.headers.length; i++) {
170 let columnParam: CsvColumnParam; 191 let columnParam: CsvColumnParam;
171 - if (isHeader && this.parseData.headers[i].search(/^(name|type|label)$/im) === 0) { 192 + let findEntityColumnType: ImportEntityColumnType;
  193 + if (isHeader) {
  194 + const headerColumnName = this.parseData.headers[i].toUpperCase();
  195 + findEntityColumnType = this.allowAssignColumn.find(column => column === headerColumnName);
  196 + }
  197 + if (isHeader && findEntityColumnType) {
172 columnParam = { 198 columnParam = {
173 - type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()], 199 + type: findEntityColumnType,
174 key: this.parseData.headers[i].toLowerCase(), 200 key: this.parseData.headers[i].toLowerCase(),
175 sampleData: this.parseData.rows[0][i] 201 sampleData: this.parseData.rows[0][i]
176 }; 202 };
@@ -188,76 +214,16 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -188,76 +214,16 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
188 214
189 215
190 private addEntities() { 216 private addEntities() {
191 - const importData = this.parseData;  
192 - const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value;  
193 - const entitiesData: ImportEntityData[] = [];  
194 - let sentDataLength = 0;  
195 - for (let row = 0; row < importData.rows.length; row++) {  
196 - const entityData: ImportEntityData = this.constructDraftImportEntityData();  
197 - const i = row;  
198 - for (let j = 0; j < parameterColumns.length; j++) {  
199 - switch (parameterColumns[j].type) {  
200 - case ImportEntityColumnType.serverAttribute:  
201 - entityData.attributes.server.push({  
202 - key: parameterColumns[j].key,  
203 - value: importData.rows[i][j]  
204 - });  
205 - break;  
206 - case ImportEntityColumnType.timeseries:  
207 - entityData.timeseries.push({  
208 - key: parameterColumns[j].key,  
209 - value: importData.rows[i][j]  
210 - });  
211 - break;  
212 - case ImportEntityColumnType.sharedAttribute:  
213 - entityData.attributes.shared.push({  
214 - key: parameterColumns[j].key,  
215 - value: importData.rows[i][j]  
216 - });  
217 - break;  
218 - case ImportEntityColumnType.accessToken:  
219 - entityData.accessToken = importData.rows[i][j];  
220 - break;  
221 - case ImportEntityColumnType.name:  
222 - entityData.name = importData.rows[i][j];  
223 - break;  
224 - case ImportEntityColumnType.type:  
225 - entityData.type = importData.rows[i][j];  
226 - break;  
227 - case ImportEntityColumnType.label:  
228 - entityData.label = importData.rows[i][j];  
229 - break;  
230 - case ImportEntityColumnType.isGateway:  
231 - entityData.gateway = importData.rows[i][j];  
232 - break;  
233 - case ImportEntityColumnType.description:  
234 - entityData.description = importData.rows[i][j];  
235 - break;  
236 - case ImportEntityColumnType.edgeLicenseKey:  
237 - (entityData as EdgeImportEntityData).edgeLicenseKey = importData.rows[i][j];  
238 - break;  
239 - case ImportEntityColumnType.cloudEndpoint:  
240 - (entityData as EdgeImportEntityData).cloudEndpoint = importData.rows[i][j];  
241 - break;  
242 - case ImportEntityColumnType.routingKey:  
243 - (entityData as EdgeImportEntityData).routingKey = importData.rows[i][j];  
244 - break;  
245 - case ImportEntityColumnType.secret:  
246 - (entityData as EdgeImportEntityData).secret = importData.rows[i][j];  
247 - break;  
248 - } 217 + const entitiesData: BulkImportRequest = {
  218 + file: this.selectFileFormGroup.get('importData').value,
  219 + mapping: {
  220 + columns: this.processingColumnsParams(),
  221 + delimiter: this.importParametersFormGroup.get('delim').value,
  222 + header: this.importParametersFormGroup.get('isHeader').value,
  223 + update: this.importParametersFormGroup.get('isUpdate').value
249 } 224 }
250 - entitiesData.push(entityData);  
251 - }  
252 - const createImportEntityCompleted = () => {  
253 - sentDataLength++;  
254 - this.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100);  
255 }; 225 };
256 -  
257 - const isUpdate: boolean = this.importParametersFormGroup.get('isUpdate').value;  
258 -  
259 - this.importExport.importEntities(entitiesData, this.entityType, isUpdate,  
260 - createImportEntityCompleted, {ignoreErrors: true, resendRequest: true}).subscribe( 226 + this.importExport.bulkImportEntities(entitiesData, this.entityType, {ignoreErrors: true}).subscribe(
261 (result) => { 227 (result) => {
262 this.statistical = result; 228 this.statistical = result;
263 this.isImportData = false; 229 this.isImportData = false;
@@ -266,31 +232,63 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom @@ -266,31 +232,63 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
266 ); 232 );
267 } 233 }
268 234
269 - private constructDraftImportEntityData(): ImportEntityData {  
270 - const entityData: ImportEntityData = {  
271 - name: '',  
272 - type: '',  
273 - description: '',  
274 - gateway: null,  
275 - label: '',  
276 - accessToken: '',  
277 - attributes: {  
278 - server: [],  
279 - shared: []  
280 - },  
281 - timeseries: []  
282 - };  
283 - if (this.entityType === EntityType.EDGE) {  
284 - const edgeEntityData: EdgeImportEntityData = entityData as EdgeImportEntityData;  
285 - edgeEntityData.edgeLicenseKey = '';  
286 - edgeEntityData.cloudEndpoint = '';  
287 - edgeEntityData.routingKey = '';  
288 - edgeEntityData.secret = '';  
289 - return edgeEntityData;  
290 - } else {  
291 - return entityData; 235 + private processingColumnsParams(): Array<ColumnMapping> {
  236 + const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value;
  237 + const allowKeyForTypeColumns: ImportEntityColumnType[] = [
  238 + ImportEntityColumnType.serverAttribute,
  239 + ImportEntityColumnType.timeseries,
  240 + ImportEntityColumnType.sharedAttribute
  241 + ];
  242 + return parameterColumns.map(column => ({
  243 + type: column.type,
  244 + key: allowKeyForTypeColumns.some(type => type === column.type) ? column.key : undefined
  245 + }));
  246 + }
  247 +
  248 + initEditor() {
  249 + if (!this.initEditorComponent) {
  250 + this.createEditor(this.failureDetailsEditorElmRef, this.statistical.errorsList);
292 } 251 }
293 } 252 }
294 253
  254 + private createEditor(editorElementRef: ElementRef, contents: string[]): void {
  255 + const editorElement = editorElementRef.nativeElement;
  256 + let editorOptions: Partial<Ace.EditorOptions> = {
  257 + mode: 'ace/mode/java',
  258 + theme: 'ace/theme/github',
  259 + showGutter: false,
  260 + showPrintMargin: false,
  261 + readOnly: true
  262 + };
  263 +
  264 + const advancedOptions = {
  265 + enableSnippets: false,
  266 + enableBasicAutocompletion: false,
  267 + enableLiveAutocompletion: false
  268 + };
  269 +
  270 + editorOptions = {...editorOptions, ...advancedOptions};
  271 + const content = contents.map(error => error.replace('\n', '')).join('\n');
  272 + getAce().subscribe(
  273 + (ace) => {
  274 + const editor = ace.edit(editorElement, editorOptions);
  275 + editor.session.setUseWrapMode(false);
  276 + editor.setValue(content, -1);
  277 + this.updateEditorSize(editorElement, content, editor);
  278 + }
  279 + );
  280 + }
  281 +
  282 + private updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) {
  283 + let newHeight = 200;
  284 + if (content && content.length > 0) {
  285 + const lines = content.split('\n');
  286 + newHeight = 16 * lines.length + 24;
  287 + }
  288 + const minHeight = Math.min(200, newHeight);
  289 + this.renderer.setStyle(editorElement, 'minHeight', minHeight.toString() + 'px');
  290 + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
  291 + editor.resize();
  292 + }
295 293
296 } 294 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Widget, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models'; 17 +import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models';
18 import { DashboardLayoutId } from '@shared/models/dashboard.models'; 18 import { DashboardLayoutId } from '@shared/models/dashboard.models';
19 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 19 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
20 20
@@ -38,6 +38,8 @@ export interface CsvToJsonResult { @@ -38,6 +38,8 @@ export interface CsvToJsonResult {
38 rows?: any[][]; 38 rows?: any[][];
39 } 39 }
40 40
  41 +export type CSVDelimiter = ',' | ';' | '|' | '\t';
  42 +
41 export enum ImportEntityColumnType { 43 export enum ImportEntityColumnType {
42 name = 'NAME', 44 name = 'NAME',
43 type = 'TYPE', 45 type = 'TYPE',
@@ -46,8 +48,22 @@ export enum ImportEntityColumnType { @@ -46,8 +48,22 @@ export enum ImportEntityColumnType {
46 sharedAttribute = 'SHARED_ATTRIBUTE', 48 sharedAttribute = 'SHARED_ATTRIBUTE',
47 serverAttribute = 'SERVER_ATTRIBUTE', 49 serverAttribute = 'SERVER_ATTRIBUTE',
48 timeseries = 'TIMESERIES', 50 timeseries = 'TIMESERIES',
49 - entityField = 'ENTITY_FIELD',  
50 accessToken = 'ACCESS_TOKEN', 51 accessToken = 'ACCESS_TOKEN',
  52 + x509 = 'X509',
  53 + mqttClientId = 'MQTT_CLIENT_ID',
  54 + mqttUserName = 'MQTT_USER_NAME',
  55 + mqttPassword = 'MQTT_PASSWORD',
  56 + lwm2mClientEndpoint = 'LWM2M_CLIENT_ENDPOINT',
  57 + lwm2mClientSecurityConfigMode = 'LWM2M_CLIENT_SECURITY_CONFIG_MODE',
  58 + lwm2mClientIdentity = 'LWM2M_CLIENT_IDENTITY',
  59 + lwm2mClientKey = 'LWM2M_CLIENT_KEY',
  60 + lwm2mClientCert = 'LWM2M_CLIENT_CERT',
  61 + lwm2mBootstrapServerSecurityMode = 'LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE',
  62 + lwm2mBootstrapServerClientPublicKeyOrId = 'LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID',
  63 + lwm2mBootstrapServerClientSecretKey = 'LWM2M_BOOTSTRAP_SERVER_SECRET_KEY',
  64 + lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
  65 + lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
  66 + lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
51 isGateway = 'IS_GATEWAY', 67 isGateway = 'IS_GATEWAY',
52 description = 'DESCRIPTION', 68 description = 'DESCRIPTION',
53 edgeLicenseKey = 'EDGE_LICENSE_KEY', 69 edgeLicenseKey = 'EDGE_LICENSE_KEY',
@@ -68,8 +84,22 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType @@ -68,8 +84,22 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
68 [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'], 84 [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'],
69 [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'], 85 [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'],
70 [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'], 86 [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'],
71 - [ImportEntityColumnType.entityField, 'import.column-type.entity-field'],  
72 [ImportEntityColumnType.accessToken, 'import.column-type.access-token'], 87 [ImportEntityColumnType.accessToken, 'import.column-type.access-token'],
  88 + [ImportEntityColumnType.x509, 'import.column-type.x509'],
  89 + [ImportEntityColumnType.mqttClientId, 'import.column-type.mqtt.client-id'],
  90 + [ImportEntityColumnType.mqttUserName, 'import.column-type.mqtt.user-name'],
  91 + [ImportEntityColumnType.mqttPassword, 'import.column-type.mqtt.password'],
  92 + [ImportEntityColumnType.lwm2mClientEndpoint, 'import.column-type.lwm2m.client-endpoint'],
  93 + [ImportEntityColumnType.lwm2mClientSecurityConfigMode, 'import.column-type.lwm2m.security-config-mode'],
  94 + [ImportEntityColumnType.lwm2mClientIdentity, 'import.column-type.lwm2m.client-identity'],
  95 + [ImportEntityColumnType.lwm2mClientKey, 'import.column-type.lwm2m.client-key'],
  96 + [ImportEntityColumnType.lwm2mClientCert, 'import.column-type.lwm2m.client-cert'],
  97 + [ImportEntityColumnType.lwm2mBootstrapServerSecurityMode, 'import.column-type.lwm2m.bootstrap-server-security-mode'],
  98 + [ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId, 'import.column-type.lwm2m.bootstrap-server-public-key-id'],
  99 + [ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey, 'import.column-type.lwm2m.bootstrap-server-secret-key'],
  100 + [ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
  101 + [ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
  102 + [ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
73 [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'], 103 [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
74 [ImportEntityColumnType.description, 'import.column-type.description'], 104 [ImportEntityColumnType.description, 'import.column-type.description'],
75 [ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'], 105 [ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'],
@@ -85,6 +115,28 @@ export interface CsvColumnParam { @@ -85,6 +115,28 @@ export interface CsvColumnParam {
85 sampleData: any; 115 sampleData: any;
86 } 116 }
87 117
  118 +export interface ColumnMapping {
  119 + type: ImportEntityColumnType;
  120 + key?: string;
  121 +}
  122 +
  123 +export interface BulkImportRequest {
  124 + file: string;
  125 + mapping: {
  126 + columns: Array<ColumnMapping>;
  127 + delimiter: CSVDelimiter;
  128 + header: boolean;
  129 + update: boolean;
  130 + };
  131 +}
  132 +
  133 +export interface BulkImportResult {
  134 + created: number;
  135 + updated: number;
  136 + errors: number;
  137 + errorsList: Array<string>;
  138 +}
  139 +
88 export interface FileType { 140 export interface FileType {
89 mimeType: string; 141 mimeType: string;
90 extension: string; 142 extension: string;
@@ -21,7 +21,7 @@ import { Store } from '@ngrx/store'; @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
21 import { AppState } from '@core/core.state'; 21 import { AppState } from '@core/core.state';
22 import { ActionNotificationShow } from '@core/notification/notification.actions'; 22 import { ActionNotificationShow } from '@core/notification/notification.actions';
23 import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; 23 import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
24 -import { deepClone, isDefined, isObject, isUndefined } from '@core/utils'; 24 +import { deepClone, isDefined, isObject, isString, isUndefined } from '@core/utils';
25 import { WINDOW } from '@core/services/window.service'; 25 import { WINDOW } from '@core/services/window.service';
26 import { DOCUMENT } from '@angular/common'; 26 import { DOCUMENT } from '@angular/common';
27 import { 27 import {
@@ -44,7 +44,7 @@ import { @@ -44,7 +44,7 @@ import {
44 EntityAliasesDialogData 44 EntityAliasesDialogData
45 } from '@home/components/alias/entity-aliases-dialog.component'; 45 } from '@home/components/alias/entity-aliases-dialog.component';
46 import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; 46 import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
47 -import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE } from './import-export.models'; 47 +import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE, BulkImportRequest, BulkImportResult } from './import-export.models';
48 import { EntityType } from '@shared/models/entity-type.models'; 48 import { EntityType } from '@shared/models/entity-type.models';
49 import { UtilsService } from '@core/services/utils.service'; 49 import { UtilsService } from '@core/services/utils.service';
50 import { WidgetService } from '@core/http/widget.service'; 50 import { WidgetService } from '@core/http/widget.service';
@@ -59,6 +59,9 @@ import { DeviceProfileService } from '@core/http/device-profile.service'; @@ -59,6 +59,9 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
59 import { DeviceProfile } from '@shared/models/device.models'; 59 import { DeviceProfile } from '@shared/models/device.models';
60 import { TenantProfile } from '@shared/models/tenant.model'; 60 import { TenantProfile } from '@shared/models/tenant.model';
61 import { TenantProfileService } from '@core/http/tenant-profile.service'; 61 import { TenantProfileService } from '@core/http/tenant-profile.service';
  62 +import { DeviceService } from '@core/http/device.service';
  63 +import { AssetService } from '@core/http/asset.service';
  64 +import { EdgeService } from '@core/http/edge.service';
62 65
63 // @dynamic 66 // @dynamic
64 @Injectable() 67 @Injectable()
@@ -75,6 +78,9 @@ export class ImportExportService { @@ -75,6 +78,9 @@ export class ImportExportService {
75 private tenantProfileService: TenantProfileService, 78 private tenantProfileService: TenantProfileService,
76 private entityService: EntityService, 79 private entityService: EntityService,
77 private ruleChainService: RuleChainService, 80 private ruleChainService: RuleChainService,
  81 + private deviceService: DeviceService,
  82 + private assetService: AssetService,
  83 + private edgeService: EdgeService,
78 private utils: UtilsService, 84 private utils: UtilsService,
79 private itembuffer: ItemBufferService, 85 private itembuffer: ItemBufferService,
80 private dialog: MatDialog) { 86 private dialog: MatDialog) {
@@ -342,6 +348,17 @@ export class ImportExportService { @@ -342,6 +348,17 @@ export class ImportExportService {
342 ); 348 );
343 } 349 }
344 350
  351 + public bulkImportEntities(entitiesData: BulkImportRequest, entityType: EntityType, config?: RequestConfig): Observable<BulkImportResult> {
  352 + switch (entityType) {
  353 + case EntityType.DEVICE:
  354 + return this.deviceService.bulkImportDevices(entitiesData, config);
  355 + case EntityType.ASSET:
  356 + return this.assetService.bulkImportAssets(entitiesData, config);
  357 + case EntityType.EDGE:
  358 + return this.edgeService.bulkImportEdges(entitiesData, config);
  359 + }
  360 + }
  361 +
345 public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean, 362 public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean,
346 importEntityCompleted?: () => void, config?: RequestConfig): Observable<ImportEntitiesResultInfo> { 363 importEntityCompleted?: () => void, config?: RequestConfig): Observable<ImportEntitiesResultInfo> {
347 let partSize = 100; 364 let partSize = 100;
@@ -563,6 +580,8 @@ export class ImportExportService { @@ -563,6 +580,8 @@ export class ImportExportService {
563 if (isObject(obj2[key])) { 580 if (isObject(obj2[key])) {
564 obj1[key] = obj1[key] || {}; 581 obj1[key] = obj1[key] || {};
565 obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])}; 582 obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])};
  583 + } else if (isString(obj2[key])) {
  584 + obj1[key] = (obj1[key] || '') + `${obj2[key]}\n`;
566 } else { 585 } else {
567 obj1[key] = (obj1[key] || 0) + obj2[key]; 586 obj1[key] = (obj1[key] || 0) + obj2[key];
568 } 587 }
@@ -23,23 +23,28 @@ @@ -23,23 +23,28 @@
23 </mat-cell> 23 </mat-cell>
24 </ng-container> 24 </ng-container>
25 <ng-container matColumnDef="sampleData"> 25 <ng-container matColumnDef="sampleData">
26 - <mat-header-cell *matHeaderCellDef style="flex: 0 0 30%;" class="mat-column-sampleData"> {{ 'import.column-example' | translate }} </mat-header-cell> 26 + <mat-header-cell *matHeaderCellDef class="mat-column-sampleData"> {{ 'import.column-example' | translate }} </mat-header-cell>
27 <mat-cell *matCellDef="let column"> 27 <mat-cell *matCellDef="let column">
28 {{column.sampleData}} 28 {{column.sampleData}}
29 </mat-cell> 29 </mat-cell>
30 </ng-container> 30 </ng-container>
31 <ng-container matColumnDef="type"> 31 <ng-container matColumnDef="type">
32 - <mat-header-cell *matHeaderCellDef style="flex: 0 0 40%" class="mat-column-type"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell> 32 + <mat-header-cell *matHeaderCellDef class="mat-column-type"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell>
33 <mat-cell *matCellDef="let column"> 33 <mat-cell *matCellDef="let column">
34 - <mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()"> 34 + <mat-select [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
35 <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled"> 35 <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled">
36 {{ columnTypesTranslations.get(type.value) | translate }} 36 {{ columnTypesTranslations.get(type.value) | translate }}
37 </mat-option> 37 </mat-option>
  38 + <mat-optgroup label="{{ 'import.credentials' | translate }}" *ngIf="entityType === entityTypeDevice">
  39 + <mat-option *ngFor="let credential of columnDeviceCredentials" [value]="credential.value" [disabled]="credential.disabled">
  40 + {{ columnTypesTranslations.get(credential.value) | translate }}
  41 + </mat-option>
  42 + </mat-optgroup>
38 </mat-select> 43 </mat-select>
39 </mat-cell> 44 </mat-cell>
40 </ng-container> 45 </ng-container>
41 <ng-container matColumnDef="key"> 46 <ng-container matColumnDef="key">
42 - <mat-header-cell *matHeaderCellDef style="flex: 0 0 30%"> {{ 'import.column-key' | translate }} </mat-header-cell> 47 + <mat-header-cell *matHeaderCellDef class="mat-column-key"> {{ 'import.column-key' | translate }} </mat-header-cell>
43 <mat-cell *matCellDef="let column"> 48 <mat-cell *matCellDef="let column">
44 <mat-form-field floatLabel="always" hideRequiredMarker 49 <mat-form-field floatLabel="always" hideRequiredMarker
45 *ngIf="isColumnTypeDiffers(column.type)"> 50 *ngIf="isColumnTypeDiffers(column.type)">
@@ -14,14 +14,21 @@ @@ -14,14 +14,21 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 :host { 16 :host {
  17 + overflow-x: auto;
  18 +
17 .mat-column-order { 19 .mat-column-order {
18 flex: 0 0 40px; 20 flex: 0 0 40px;
19 } 21 }
20 .mat-column-sampleData { 22 .mat-column-sampleData {
21 flex: 0 0 120px; 23 flex: 0 0 120px;
22 min-width: 120px; 24 min-width: 120px;
  25 + max-width: 230px;
23 } 26 }
24 .mat-column-type { 27 .mat-column-type {
  28 + flex: 0 0 180px;
  29 + min-width: 180px;
  30 + }
  31 + .mat-column-key {
25 flex: 0 0 120px; 32 flex: 0 0 120px;
26 min-width: 120px; 33 min-width: 120px;
27 } 34 }
@@ -57,8 +57,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce @@ -57,8 +57,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
57 57
58 columnTypes: AssignmentColumnType[] = []; 58 columnTypes: AssignmentColumnType[] = [];
59 59
  60 + columnDeviceCredentials: AssignmentColumnType[] = [];
  61 +
60 columnTypesTranslations = importEntityColumnTypeTranslations; 62 columnTypesTranslations = importEntityColumnTypeTranslations;
61 63
  64 + readonly entityTypeDevice = EntityType.DEVICE;
  65 +
62 private columns: CsvColumnParam[]; 66 private columns: CsvColumnParam[];
63 67
64 private valid = true; 68 private valid = true;
@@ -83,9 +87,26 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce @@ -83,9 +87,26 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
83 { value: ImportEntityColumnType.sharedAttribute }, 87 { value: ImportEntityColumnType.sharedAttribute },
84 { value: ImportEntityColumnType.serverAttribute }, 88 { value: ImportEntityColumnType.serverAttribute },
85 { value: ImportEntityColumnType.timeseries }, 89 { value: ImportEntityColumnType.timeseries },
86 - { value: ImportEntityColumnType.accessToken },  
87 { value: ImportEntityColumnType.isGateway } 90 { value: ImportEntityColumnType.isGateway }
88 ); 91 );
  92 + this.columnDeviceCredentials.push(
  93 + { value: ImportEntityColumnType.accessToken },
  94 + { value: ImportEntityColumnType.x509 },
  95 + { value: ImportEntityColumnType.mqttClientId },
  96 + { value: ImportEntityColumnType.mqttUserName },
  97 + { value: ImportEntityColumnType.mqttPassword },
  98 + { value: ImportEntityColumnType.lwm2mClientEndpoint },
  99 + { value: ImportEntityColumnType.lwm2mClientSecurityConfigMode },
  100 + { value: ImportEntityColumnType.lwm2mClientIdentity },
  101 + { value: ImportEntityColumnType.lwm2mClientKey },
  102 + { value: ImportEntityColumnType.lwm2mClientCert },
  103 + { value: ImportEntityColumnType.lwm2mBootstrapServerSecurityMode },
  104 + { value: ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId },
  105 + { value: ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey },
  106 + { value: ImportEntityColumnType.lwm2mServerSecurityMode },
  107 + { value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
  108 + { value: ImportEntityColumnType.lwm2mServerClientSecretKey },
  109 + );
89 break; 110 break;
90 case EntityType.ASSET: 111 case EntityType.ASSET:
91 this.columnTypes.push( 112 this.columnTypes.push(
@@ -123,8 +144,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce @@ -123,8 +144,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
123 const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1; 144 const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1;
124 const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1; 145 const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1;
125 const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1; 146 const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1;
126 - const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1;  
127 - const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;  
128 const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1; 147 const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1;
129 const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1; 148 const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1;
130 const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1; 149 const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1;
@@ -139,14 +158,19 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce @@ -139,14 +158,19 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
139 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel; 158 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel;
140 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription; 159 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription;
141 160
142 - const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);  
143 - if (isGatewayColumnType) {  
144 - isGatewayColumnType.disabled = isSelectGateway;  
145 - }  
146 - const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken);  
147 - if (accessTokenColumnType) {  
148 - accessTokenColumnType.disabled = isSelectCredentials; 161 + if (this.entityType === EntityType.DEVICE) {
  162 + const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;
  163 +
  164 + const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);
  165 + if (isGatewayColumnType) {
  166 + isGatewayColumnType.disabled = isSelectGateway;
  167 + }
  168 +
  169 + this.columnDeviceCredentials.forEach((columnCredential) => {
  170 + columnCredential.disabled = this.columns.findIndex(column => column.type === columnCredential.value) > -1;
  171 + });
149 } 172 }
  173 +
150 const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey); 174 const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey);
151 if (edgeLicenseKeyColumnType) { 175 if (edgeLicenseKeyColumnType) {
152 edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey; 176 edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey;
@@ -743,6 +743,14 @@ export interface DeviceCredentialMQTTBasic { @@ -743,6 +743,14 @@ export interface DeviceCredentialMQTTBasic {
743 password: string; 743 password: string;
744 } 744 }
745 745
  746 +export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
  747 + return {
  748 + clientId: '',
  749 + userName: '',
  750 + password: ''
  751 + };
  752 +}
  753 +
746 export interface DeviceSearchQuery extends EntitySearchQuery { 754 export interface DeviceSearchQuery extends EntitySearchQuery {
747 deviceTypes: Array<string>; 755 deviceTypes: Array<string>;
748 } 756 }
@@ -17,6 +17,8 @@ @@ -17,6 +17,8 @@
17 import { EntityType } from '@shared/models/entity-type.models'; 17 import { EntityType } from '@shared/models/entity-type.models';
18 import { AttributeData } from './telemetry/telemetry.models'; 18 import { AttributeData } from './telemetry/telemetry.models';
19 import { EntityId } from '@shared/models/id/entity-id'; 19 import { EntityId } from '@shared/models/id/entity-id';
  20 +import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
  21 +import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models';
20 22
21 export interface EntityInfo { 23 export interface EntityInfo {
22 name?: string; 24 name?: string;
@@ -32,12 +34,18 @@ export interface EntityInfoData { @@ -32,12 +34,18 @@ export interface EntityInfoData {
32 } 34 }
33 35
34 export interface ImportEntityData { 36 export interface ImportEntityData {
  37 + lineNumber: number;
35 name: string; 38 name: string;
36 type: string; 39 type: string;
37 label: string; 40 label: string;
38 gateway: boolean; 41 gateway: boolean;
39 description: string; 42 description: string;
40 - accessToken: string; 43 + credential: {
  44 + accessToken?: string;
  45 + x509?: string;
  46 + mqtt?: DeviceCredentialMQTTBasic;
  47 + lwm2m?: Lwm2mSecurityConfigModels;
  48 + };
41 attributes: { 49 attributes: {
42 server: AttributeData[], 50 server: AttributeData[],
43 shared: AttributeData[] 51 shared: AttributeData[]
@@ -61,6 +69,7 @@ export interface ImportEntitiesResultInfo { @@ -61,6 +69,7 @@ export interface ImportEntitiesResultInfo {
61 }; 69 };
62 error?: { 70 error?: {
63 entity: number; 71 entity: number;
  72 + errors?: string;
64 }; 73 };
65 } 74 }
66 75
@@ -58,6 +58,20 @@ export interface Lwm2mSecurityConfigModels { @@ -58,6 +58,20 @@ export interface Lwm2mSecurityConfigModels {
58 bootstrap: BootstrapSecurityConfig; 58 bootstrap: BootstrapSecurityConfig;
59 } 59 }
60 60
  61 +
  62 +export function getLwm2mSecurityConfigModelsDefault(): Lwm2mSecurityConfigModels {
  63 + return {
  64 + client: {
  65 + securityConfigClientMode: Lwm2mSecurityType.NO_SEC,
  66 + endpoint: ''
  67 + },
  68 + bootstrap: {
  69 + bootstrapServer: getDefaultServerSecurityConfig(),
  70 + lwm2mServer: getDefaultServerSecurityConfig()
  71 + }
  72 + };
  73 +}
  74 +
61 export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig { 75 export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig {
62 let security = { 76 let security = {
63 securityConfigClientMode: securityConfigMode, 77 securityConfigClientMode: securityConfigMode,
@@ -2187,9 +2187,11 @@ @@ -2187,9 +2187,11 @@
2187 "column-title": "Title", 2187 "column-title": "Title",
2188 "column-example": "Example value data", 2188 "column-example": "Example value data",
2189 "column-key": "Attribute/telemetry key", 2189 "column-key": "Attribute/telemetry key",
  2190 + "credentials": "Credentials",
2190 "csv-delimiter": "CSV delimiter", 2191 "csv-delimiter": "CSV delimiter",
2191 "csv-first-line-header": "First line contains column names", 2192 "csv-first-line-header": "First line contains column names",
2192 "csv-update-data": "Update attributes/telemetry", 2193 "csv-update-data": "Update attributes/telemetry",
  2194 + "details": "Details",
2193 "import-csv-number-columns-error": "A file should contain at least two columns", 2195 "import-csv-number-columns-error": "A file should contain at least two columns",
2194 "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", 2196 "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'",
2195 "column-type": { 2197 "column-type": {
@@ -2203,6 +2205,25 @@ @@ -2203,6 +2205,25 @@
2203 "timeseries": "Timeseries", 2205 "timeseries": "Timeseries",
2204 "entity-field": "Entity field", 2206 "entity-field": "Entity field",
2205 "access-token": "Access token", 2207 "access-token": "Access token",
  2208 + "x509": "X.509",
  2209 + "mqtt": {
  2210 + "client-id": "MQTT client ID",
  2211 + "user-name": "MQTT user name",
  2212 + "password": "MQTT password"
  2213 + },
  2214 + "lwm2m": {
  2215 + "client-endpoint": "LwM2M endpoint client name",
  2216 + "security-config-mode": "LwM2M security config mode",
  2217 + "client-identity": "LwM2M client identity",
  2218 + "client-key": "LwM2M client key",
  2219 + "client-cert": "LwM2M client public key",
  2220 + "bootstrap-server-security-mode": "LwM2M bootstrap server security mode",
  2221 + "bootstrap-server-secret-key": "LwM2M bootstrap server secret key",
  2222 + "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id",
  2223 + "lwm2m-server-security-mode": "LwM2M server security mode",
  2224 + "lwm2m-server-secret-key": "LwM2M server secret key",
  2225 + "lwm2m-server-public-key-id": "LwM2M server public key or id"
  2226 + },
2206 "isgateway": "Is Gateway", 2227 "isgateway": "Is Gateway",
2207 "activity-time-from-gateway-device": "Activity time from gateway device", 2228 "activity-time-from-gateway-device": "Activity time from gateway device",
2208 "description": "Description", 2229 "description": "Description",