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 16 package org.thingsboard.server.controller;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import lombok.RequiredArgsConstructor;
  20 +import lombok.extern.slf4j.Slf4j;
19 21 import org.springframework.http.HttpStatus;
20 22 import org.springframework.security.access.prepost.PreAuthorize;
21 23 import org.springframework.web.bind.annotation.PathVariable;
  24 +import org.springframework.web.bind.annotation.PostMapping;
22 25 import org.springframework.web.bind.annotation.RequestBody;
23 26 import org.springframework.web.bind.annotation.RequestMapping;
24 27 import org.springframework.web.bind.annotation.RequestMethod;
... ... @@ -34,7 +37,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
34 37 import org.thingsboard.server.common.data.asset.AssetSearchQuery;
35 38 import org.thingsboard.server.common.data.audit.ActionType;
36 39 import org.thingsboard.server.common.data.edge.Edge;
37   -import org.thingsboard.server.common.data.edge.EdgeEventType;
38 40 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 41 import org.thingsboard.server.common.data.edge.EdgeEventActionType;
40 42 import org.thingsboard.server.common.data.exception.ThingsboardException;
... ... @@ -48,6 +50,9 @@ import org.thingsboard.server.common.data.page.TimePageLink;
48 50 import org.thingsboard.server.dao.exception.IncorrectParameterException;
49 51 import org.thingsboard.server.dao.model.ModelConstants;
50 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 56 import org.thingsboard.server.service.security.model.SecurityUser;
52 57 import org.thingsboard.server.service.security.permission.Operation;
53 58 import org.thingsboard.server.service.security.permission.Resource;
... ... @@ -63,7 +68,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
63 68 @RestController
64 69 @TbCoreComponent
65 70 @RequestMapping("/api")
  71 +@RequiredArgsConstructor
  72 +@Slf4j
66 73 public class AssetController extends BaseController {
  74 + private final AssetBulkImportService assetBulkImportService;
67 75
68 76 public static final String ASSET_ID = "assetId";
69 77
... ... @@ -108,13 +116,7 @@ public class AssetController extends BaseController {
108 116
109 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 121 return savedAsset;
120 122 } catch (Exception e) {
... ... @@ -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 143 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
128 144 @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
129 145 @ResponseStatus(value = HttpStatus.OK)
... ... @@ -258,7 +274,7 @@ public class AssetController extends BaseController {
258 274 try {
259 275 TenantId tenantId = getCurrentUser().getTenantId();
260 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 278 return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
263 279 } else {
264 280 return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
... ... @@ -321,7 +337,7 @@ public class AssetController extends BaseController {
321 337 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
322 338 checkCustomerId(customerId, Operation.READ);
323 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 341 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
326 342 } else {
327 343 return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
... ... @@ -426,7 +442,7 @@ public class AssetController extends BaseController {
426 442 @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST)
427 443 @ResponseBody
428 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 446 checkParameter(EDGE_ID, strEdgeId);
431 447 checkParameter(ASSET_ID, strAssetId);
432 448 try {
... ... @@ -444,7 +460,7 @@ public class AssetController extends BaseController {
444 460
445 461 sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedAsset.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE);
446 462
447   - return savedAsset;
  463 + return savedAsset;
448 464 } catch (Exception e) {
449 465
450 466 logEntityAction(emptyId(EntityType.ASSET), null,
... ... @@ -530,4 +546,13 @@ public class AssetController extends BaseController {
530 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 121 import org.thingsboard.server.queue.discovery.PartitionService;
122 122 import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
123 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 125 import org.thingsboard.server.service.component.ComponentDiscoveryService;
126 126 import org.thingsboard.server.service.edge.EdgeLicenseService;
127 127 import org.thingsboard.server.service.edge.EdgeNotificationService;
... ... @@ -279,7 +279,7 @@ public abstract class BaseController {
279 279 protected EdgeLicenseService edgeLicenseService;
280 280
281 281 @Autowired
282   - protected RuleEngineEntityActionService ruleEngineEntityActionService;
  282 + protected EntityActionService entityActionService;
283 283
284 284 @Value("${server.log_controller_error_stack_trace}")
285 285 @Getter
... ... @@ -819,13 +819,7 @@ public abstract class BaseController {
819 819
820 820 protected <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
821 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 19 import com.google.common.util.concurrent.Futures;
20 20 import com.google.common.util.concurrent.ListenableFuture;
21 21 import com.google.common.util.concurrent.MoreExecutors;
  22 +import lombok.RequiredArgsConstructor;
  23 +import lombok.extern.slf4j.Slf4j;
22 24 import org.springframework.http.HttpStatus;
23 25 import org.springframework.http.ResponseEntity;
24 26 import org.springframework.security.access.prepost.PreAuthorize;
25 27 import org.springframework.web.bind.annotation.PathVariable;
  28 +import org.springframework.web.bind.annotation.PostMapping;
26 29 import org.springframework.web.bind.annotation.RequestBody;
27 30 import org.springframework.web.bind.annotation.RequestMapping;
28 31 import org.springframework.web.bind.annotation.RequestMethod;
... ... @@ -56,7 +59,6 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
56 59 import org.thingsboard.server.common.data.page.PageData;
57 60 import org.thingsboard.server.common.data.page.PageLink;
58 61 import org.thingsboard.server.common.data.page.TimePageLink;
59   -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
60 62 import org.thingsboard.server.common.data.security.DeviceCredentials;
61 63 import org.thingsboard.server.common.msg.TbMsg;
62 64 import org.thingsboard.server.common.msg.TbMsgDataType;
... ... @@ -67,6 +69,9 @@ import org.thingsboard.server.dao.device.claim.ReclaimResult;
67 69 import org.thingsboard.server.dao.exception.IncorrectParameterException;
68 70 import org.thingsboard.server.dao.model.ModelConstants;
69 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 75 import org.thingsboard.server.service.security.model.SecurityUser;
71 76 import org.thingsboard.server.service.security.permission.Operation;
72 77 import org.thingsboard.server.service.security.permission.Resource;
... ... @@ -83,7 +88,10 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
83 88 @RestController
84 89 @TbCoreComponent
85 90 @RequestMapping("/api")
  91 +@RequiredArgsConstructor
  92 +@Slf4j
86 93 public class DeviceController extends BaseController {
  94 + private final DeviceBulkImportService deviceBulkImportService;
87 95
88 96 private static final String DEVICE_ID = "deviceId";
89 97 private static final String DEVICE_NAME = "deviceName";
... ... @@ -133,11 +141,7 @@ public class DeviceController extends BaseController {
133 141
134 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 146 return savedDevice;
143 147 } catch (Exception e) {
... ... @@ -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 167 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
152 168 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE)
153 169 @ResponseStatus(value = HttpStatus.OK)
... ... @@ -776,4 +792,13 @@ public class DeviceController extends BaseController {
776 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 17
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import lombok.RequiredArgsConstructor;
20 21 import lombok.extern.slf4j.Slf4j;
21 22 import org.springframework.http.HttpStatus;
22 23 import org.springframework.http.ResponseEntity;
23 24 import org.springframework.security.access.prepost.PreAuthorize;
24 25 import org.springframework.web.bind.annotation.PathVariable;
  26 +import org.springframework.web.bind.annotation.PostMapping;
25 27 import org.springframework.web.bind.annotation.RequestBody;
26 28 import org.springframework.web.bind.annotation.RequestMapping;
27 29 import org.springframework.web.bind.annotation.RequestMethod;
... ... @@ -52,10 +54,14 @@ import org.thingsboard.server.dao.exception.DataValidationException;
52 54 import org.thingsboard.server.dao.exception.IncorrectParameterException;
53 55 import org.thingsboard.server.dao.model.ModelConstants;
54 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 60 import org.thingsboard.server.service.security.model.SecurityUser;
56 61 import org.thingsboard.server.service.security.permission.Operation;
57 62 import org.thingsboard.server.service.security.permission.Resource;
58 63
  64 +import java.io.IOException;
59 65 import java.util.ArrayList;
60 66 import java.util.List;
61 67 import java.util.stream.Collectors;
... ... @@ -64,7 +70,9 @@ import java.util.stream.Collectors;
64 70 @TbCoreComponent
65 71 @Slf4j
66 72 @RequestMapping("/api")
  73 +@RequiredArgsConstructor
67 74 public class EdgeController extends BaseController {
  75 + private final EdgeBulkImportService edgeBulkImportService;
68 76
69 77 public static final String EDGE_ID = "edgeId";
70 78
... ... @@ -132,17 +140,8 @@ public class EdgeController extends BaseController {
132 140 edge.getId(), edge);
133 141
134 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 145 return savedEdge;
147 146 } catch (Exception e) {
148 147 logEntityAction(emptyId(EntityType.EDGE), edge,
... ... @@ -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 166 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
155 167 @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE)
156 168 @ResponseStatus(value = HttpStatus.OK)
... ... @@ -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 596 private void cleanUpLicenseKey(Edge edge) {
567 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 28 import org.thingsboard.server.common.data.HasTenantId;
29 29 import org.thingsboard.server.common.data.User;
30 30 import org.thingsboard.server.common.data.audit.ActionType;
  31 +import org.thingsboard.server.common.data.exception.ThingsboardException;
31 32 import org.thingsboard.server.common.data.id.CustomerId;
32 33 import org.thingsboard.server.common.data.id.EntityId;
33 34 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
38 39 import org.thingsboard.server.common.msg.TbMsg;
39 40 import org.thingsboard.server.common.msg.TbMsgDataType;
40 41 import org.thingsboard.server.common.msg.TbMsgMetaData;
  42 +import org.thingsboard.server.dao.audit.AuditLogService;
41 43 import org.thingsboard.server.queue.util.TbCoreComponent;
42 44 import org.thingsboard.server.cluster.TbClusterService;
43 45
... ... @@ -49,8 +51,9 @@ import java.util.stream.Collectors;
49 51 @Service
50 52 @RequiredArgsConstructor
51 53 @Slf4j
52   -public class RuleEngineEntityActionService {
  54 +public class EntityActionService {
53 55 private final TbClusterService tbClusterService;
  56 + private final AuditLogService auditLogService;
54 57
55 58 private static final ObjectMapper json = new ObjectMapper();
56 59
... ... @@ -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 227 private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
214 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 26 import org.thingsboard.server.common.data.id.TenantId;
27 27 import org.thingsboard.server.common.data.page.PageData;
28 28 import org.thingsboard.server.common.data.page.PageLink;
29   -import org.thingsboard.server.common.data.page.SortOrder;
30 29 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
31 30 import org.thingsboard.server.common.msg.queue.ServiceType;
32 31 import org.thingsboard.server.dao.alarm.AlarmDao;
... ... @@ -34,17 +33,12 @@ import org.thingsboard.server.dao.alarm.AlarmService;
34 33 import org.thingsboard.server.dao.relation.RelationService;
35 34 import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
36 35 import org.thingsboard.server.dao.tenant.TenantDao;
37   -import org.thingsboard.server.dao.util.PsqlDao;
38 36 import org.thingsboard.server.queue.discovery.PartitionService;
39 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 40 import java.util.Date;
46 41 import java.util.Optional;
47   -import java.util.UUID;
48 42 import java.util.concurrent.TimeUnit;
49 43
50 44 @TbCoreComponent
... ... @@ -60,7 +54,7 @@ public class AlarmsCleanUpService {
60 54 private final AlarmDao alarmDao;
61 55 private final AlarmService alarmService;
62 56 private final RelationService relationService;
63   - private final RuleEngineEntityActionService ruleEngineEntityActionService;
  57 + private final EntityActionService entityActionService;
64 58 private final PartitionService partitionService;
65 59 private final TbTenantProfileCache tenantProfileCache;
66 60
... ... @@ -90,7 +84,7 @@ public class AlarmsCleanUpService {
90 84 toRemove.getData().forEach(alarmId -> {
91 85 relationService.deleteEntityRelations(tenantId, alarmId);
92 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 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 29
30 30 DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
31 31
  32 + void formatCredentials(DeviceCredentials deviceCredentials);
  33 +
32 34 void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
33 35 }
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
29 29
30 30 import java.io.ByteArrayInputStream;
31 31 import java.io.IOException;
  32 +import java.util.Optional;
32 33
33 34 @EqualsAndHashCode(callSuper = true)
34 35 @Slf4j
... ... @@ -83,6 +84,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
83 84 this.setDeviceData(device.getDeviceData());
84 85 this.setFirmwareId(device.getFirmwareId());
85 86 this.setSoftwareId(device.getSoftwareId());
  87 + Optional.ofNullable(device.getAdditionalInfo()).ifPresent(this::setAdditionalInfo);
86 88 return this;
87 89 }
88 90
... ...
... ... @@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
25 25 import org.thingsboard.server.common.data.id.TenantId;
26 26 import org.thingsboard.server.common.data.validation.NoXss;
27 27
  28 +import java.util.Optional;
  29 +
28 30 @EqualsAndHashCode(callSuper = true)
29 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 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 70 public TenantId getTenantId() {
60 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 16 package org.thingsboard.server.common.data.device.credentials.lwm2m;
17 17
18 18 import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
19 20 import com.fasterxml.jackson.annotation.JsonSubTypes;
20 21 import com.fasterxml.jackson.annotation.JsonTypeInfo;
21 22
... ... @@ -26,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
26 27 @JsonSubTypes.Type(value = NoSecClientCredentials.class, name = "NO_SEC"),
27 28 @JsonSubTypes.Type(value = PSKClientCredentials.class, name = "PSK"),
28 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 33 public interface LwM2MClientCredentials {
31 34
32 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 15 */
16 16 package org.thingsboard.server.common.data.edge;
17 17
18   -import com.fasterxml.jackson.databind.JsonNode;
19 18 import lombok.EqualsAndHashCode;
20 19 import lombok.Getter;
21 20 import lombok.Setter;
... ... @@ -70,6 +69,19 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
70 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 85 @Override
74 86 public String getSearchText() {
75 87 return getName();
... ...
... ... @@ -577,6 +577,14 @@ public class JsonConverter {
577 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 588 public static void setTypeCastEnabled(boolean enabled) {
581 589 isTypeCastEnabled = enabled;
582 590 }
... ...
... ... @@ -118,4 +118,9 @@ public class JacksonUtil {
118 118 public static <T> JsonNode valueToTree(T value) {
119 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 227 <groupId>org.elasticsearch.client</groupId>
228 228 <artifactId>rest</artifactId>
229 229 </dependency>
  230 + <dependency>
  231 + <groupId>org.eclipse.leshan</groupId>
  232 + <artifactId>leshan-core</artifactId>
  233 + </dependency>
230 234 </dependencies>
231 235 <build>
232 236 <plugins>
... ...
... ... @@ -16,20 +16,28 @@
16 16 package org.thingsboard.server.dao.device;
17 17
18 18
19   -import com.fasterxml.jackson.databind.node.ObjectNode;
20 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.codec.binary.Hex;
  21 +import org.eclipse.leshan.core.util.SecurityUtil;
21 22 import org.hibernate.exception.ConstraintViolationException;
22 23 import org.springframework.beans.factory.annotation.Autowired;
23 24 import org.springframework.cache.annotation.CacheEvict;
24 25 import org.springframework.cache.annotation.Cacheable;
25 26 import org.springframework.stereotype.Service;
26   -import org.springframework.util.StringUtils;
27 27 import org.thingsboard.common.util.JacksonUtil;
28 28 import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.StringUtils;
29 30 import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
  31 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapCredentials;
30 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 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 39 import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
  40 +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ServerCredentials;
33 41 import org.thingsboard.server.common.data.id.DeviceId;
34 42 import org.thingsboard.server.common.data.id.EntityId;
35 43 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -37,6 +45,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
37 45 import org.thingsboard.server.common.msg.EncryptionUtil;
38 46 import org.thingsboard.server.dao.entity.AbstractEntityService;
39 47 import org.thingsboard.server.dao.exception.DataValidationException;
  48 +import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
40 49 import org.thingsboard.server.dao.service.DataValidator;
41 50
42 51 import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE;
... ... @@ -83,17 +92,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
83 92 if (deviceCredentials.getCredentialsType() == null) {
84 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 96 log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
98 97 credentialsValidator.validate(deviceCredentials, id -> tenantId);
99 98 try {
... ... @@ -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 126 private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
113 127 BasicMqttCredentials mqttCredentials;
114 128 try {
... ... @@ -117,11 +131,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
117 131 throw new IllegalArgumentException();
118 132 }
119 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 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 144 if (StringUtils.isEmpty(mqttCredentials.getClientId())) {
126 145 deviceCredentials.setCredentialsId(mqttCredentials.getUserName());
127 146 } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) {
... ... @@ -129,7 +148,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
129 148 } else {
130 149 deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName()));
131 150 }
132   - if (!StringUtils.isEmpty(mqttCredentials.getPassword())) {
  151 + if (StringUtils.isNotEmpty(mqttCredentials.getPassword())) {
133 152 mqttCredentials.setPassword(mqttCredentials.getPassword());
134 153 }
135 154 deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials));
... ... @@ -143,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
143 162 }
144 163
145 164 private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) {
146   - LwM2MClientCredentials clientCredentials;
147   - ObjectNode json;
  165 + LwM2MDeviceCredentials lwM2MCredentials;
148 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 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 173 String credentialsId = null;
  174 + LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
162 175
163 176 switch (clientCredentials.getSecurityConfigClientMode()) {
164 177 case NO_SEC:
... ... @@ -174,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
174 187 String cert = EncryptionUtil.trimNewLines(x509Config.getCert());
175 188 String sha3Hash = EncryptionUtil.getSha3Hash(cert);
176 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 192 credentialsId = sha3Hash;
180 193 } else {
181 194 credentialsId = x509Config.getEndpoint();
... ... @@ -183,11 +196,163 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
183 196 break;
184 197 }
185 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 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 356 @Override
192 357 @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
193 358 public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
... ... @@ -201,38 +366,38 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
201 366 @Override
202 367 protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) {
203 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 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 376 @Override
212 377 protected void validateUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) {
213 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 381 DeviceCredentials existingCredentials = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
217 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 387 @Override
223 388 protected void validateDataImpl(TenantId tenantId, DeviceCredentials deviceCredentials) {
224 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 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 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 398 Device device = deviceService.findDeviceById(tenantId, deviceCredentials.getDeviceId());
234 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 228 if (foundDeviceCredentials == null) {
229 229 deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
230 230 } else {
  231 + deviceCredentials.setId(foundDeviceCredentials.getId());
231 232 deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials);
232 233 }
233 234 }
... ... @@ -241,7 +242,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
241 242 deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
242 243 deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
243 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 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 22 import { PageData } from '@shared/models/page/page-data';
23 23 import { EntitySubtype } from '@app/shared/models/entity-type.models';
24 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 27 @Injectable({
27 28 providedIn: 'root'
... ... @@ -105,4 +106,8 @@ export class AssetService {
105 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 30 } from '@app/shared/models/device.models';
31 31 import { EntitySubtype } from '@app/shared/models/entity-type.models';
32 32 import { AuthService } from '@core/auth/auth.service';
  33 +import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
33 34 import { PersistentRpc } from '@shared/models/rpc.models';
34 35
35 36 @Injectable({
... ... @@ -178,4 +179,8 @@ export class DeviceService {
178 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 23 import { EntitySubtype } from '@app/shared/models/entity-type.models';
24 24 import { Edge, EdgeEvent, EdgeInfo, EdgeSearchQuery } from '@shared/models/edge.models';
25 25 import { EntityId } from '@shared/models/id/entity-id';
  26 +import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models';
26 27
27 28 @Injectable({
28 29 providedIn: 'root'
... ... @@ -59,7 +60,7 @@ export class EdgeService {
59 60 }
60 61
61 62 public getCustomerEdgeInfos(customerId: string, pageLink: PageLink, type: string = '',
62   - config?: RequestConfig): Observable<PageData<EdgeInfo>> {
  63 + config?: RequestConfig): Observable<PageData<EdgeInfo>> {
63 64 return this.http.get<PageData<EdgeInfo>>(`/api/customer/${customerId}/edgeInfos${pageLink.toQuery()}&type=${type}`,
64 65 defaultHttpOptionsFromConfig(config));
65 66 }
... ... @@ -108,4 +109,8 @@ export class EdgeService {
108 109 public findByName(edgeName: string, config?: RequestConfig): Observable<Edge> {
109 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 59 ImportEntityData
60 60 } from '@shared/models/entity.models';
61 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 63 import { Asset } from '@shared/models/asset.models';
64 64 import { Device, DeviceCredentialsType } from '@shared/models/device.models';
65 65 import { AttributeService } from '@core/http/attribute.service';
... ... @@ -964,7 +964,12 @@ export class EntityService {
964 964 map(() => {
965 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 975 catchError(err => {
... ... @@ -988,13 +993,28 @@ export class EntityService {
988 993 map(() => {
989 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 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 1070 break;
1051 1071 }
1052 1072 return saveEntityObservable;
1053   -
1054 1073 }
1055 1074
1056 1075 private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData,
... ... @@ -1123,15 +1142,31 @@ export class EntityService {
1123 1142 public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> {
1124 1143 const observables: Observable<string>[] = [];
1125 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 1162 observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe(
1128 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 1167 return this.deviceService.saveDeviceCredentials(credentials, config).pipe(
1133 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 1176 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
1142 1177 entityData.attributes.shared, config).pipe(
1143 1178 map(() => 'ok'),
1144   - catchError(err => of('error'))
  1179 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1145 1180 );
1146 1181 observables.push(observable);
1147 1182 }
... ... @@ -1149,23 +1184,23 @@ export class EntityService {
1149 1184 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE,
1150 1185 entityData.attributes.server, config).pipe(
1151 1186 map(() => 'ok'),
1152   - catchError(err => of('error'))
  1187 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1153 1188 );
1154 1189 observables.push(observable);
1155 1190 }
1156 1191 if (entityData.timeseries && entityData.timeseries.length) {
1157 1192 observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe(
1158 1193 map(() => 'ok'),
1159   - catchError(err => of('error'))
  1194 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1160 1195 );
1161 1196 observables.push(observable);
1162 1197 }
1163 1198 if (observables.length) {
1164 1199 return forkJoin(observables).pipe(
1165 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 1204 } else {
1170 1205 return response;
1171 1206 }
... ...
... ... @@ -94,7 +94,7 @@
94 94 <mat-step [stepControl]="columnTypesFormGroup">
95 95 <form [formGroup]="columnTypesFormGroup">
96 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 98 </form>
99 99 <div fxLayout="row wrap" fxLayoutAlign="space-between center">
100 100 <button mat-button
... ... @@ -113,21 +113,32 @@
113 113 </mat-step>
114 114 <mat-step>
115 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 117 </mat-progress-bar>
118 118 </mat-step>
119 119 <mat-step>
120 120 <ng-template matStepLabel>{{ 'import.stepper-text.done' | translate }}</ng-template>
121 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 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 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 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 142 </div>
132 143 <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
133 144 <button mat-raised-button
... ...
... ... @@ -26,5 +26,30 @@
26 26 .tb-import-progress{
27 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 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 18 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -26,14 +26,20 @@ import { TranslateService } from '@ngx-translate/core';
26 26 import { ActionNotificationShow } from '@core/notification/notification.actions';
27 27 import { MatVerticalStepper } from '@angular/material/stepper';
28 28 import {
  29 + BulkImportRequest,
  30 + BulkImportResult,
  31 + ColumnMapping,
29 32 convertCSVToJson,
30 33 CsvColumnParam,
  34 + CSVDelimiter,
31 35 CsvToJsonConfig,
32 36 CsvToJsonResult,
33 37 ImportEntityColumnType
34 38 } from '@home/components/import-export/import-export.models';
35   -import { EdgeImportEntityData, ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models';
36 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 44 export interface ImportDialogCsvData {
39 45 entityType: EntityType;
... ... @@ -48,15 +54,21 @@ export interface ImportDialogCsvData {
48 54 styleUrls: ['./import-dialog-csv.component.scss']
49 55 })
50 56 export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean>
51   - implements OnInit {
  57 + implements AfterViewInit {
52 58
53 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 67 entityType: EntityType;
56 68 importTitle: string;
57 69 importFileLabel: string;
58 70
59   - delimiters: {key: string, value: string}[] = [{
  71 + delimiters: { key: CSVDelimiter, value: string }[] = [{
60 72 key: ',',
61 73 value: ','
62 74 }, {
... ... @@ -77,9 +89,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
77 89 columnTypesFormGroup: FormGroup;
78 90
79 91 isImportData = false;
80   - progressCreate = 0;
81   - statistical: ImportEntitiesResultInfo;
  92 + statistical: BulkImportResult;
82 93
  94 + private allowAssignColumn: ImportEntityColumnType[];
  95 + private initEditorComponent = false;
83 96 private parseData: CsvToJsonResult;
84 97
85 98 constructor(protected store: Store<AppState>,
... ... @@ -88,7 +101,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
88 101 public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>,
89 102 public translate: TranslateService,
90 103 private importExport: ImportExportService,
91   - private fb: FormBuilder) {
  104 + private fb: FormBuilder,
  105 + private renderer: Renderer2) {
92 106 super(store, router, dialogRef);
93 107 this.entityType = data.entityType;
94 108 this.importTitle = data.importTitle;
... ... @@ -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 134 cancel(): void {
... ... @@ -157,8 +176,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
157 176 return convertCSVToJson(importData, config,
158 177 (messageId, params) => {
159 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 189 const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value;
169 190 for (let i = 0; i < this.parseData.headers.length; i++) {
170 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 198 columnParam = {
173   - type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()],
  199 + type: findEntityColumnType,
174 200 key: this.parseData.headers[i].toLowerCase(),
175 201 sampleData: this.parseData.rows[0][i]
176 202 };
... ... @@ -188,76 +214,16 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
188 214
189 215
190 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 227 (result) => {
262 228 this.statistical = result;
263 229 this.isImportData = false;
... ... @@ -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 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 18 import { DashboardLayoutId } from '@shared/models/dashboard.models';
19 19 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
20 20
... ... @@ -38,6 +38,8 @@ export interface CsvToJsonResult {
38 38 rows?: any[][];
39 39 }
40 40
  41 +export type CSVDelimiter = ',' | ';' | '|' | '\t';
  42 +
41 43 export enum ImportEntityColumnType {
42 44 name = 'NAME',
43 45 type = 'TYPE',
... ... @@ -46,8 +48,22 @@ export enum ImportEntityColumnType {
46 48 sharedAttribute = 'SHARED_ATTRIBUTE',
47 49 serverAttribute = 'SERVER_ATTRIBUTE',
48 50 timeseries = 'TIMESERIES',
49   - entityField = 'ENTITY_FIELD',
50 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 67 isGateway = 'IS_GATEWAY',
52 68 description = 'DESCRIPTION',
53 69 edgeLicenseKey = 'EDGE_LICENSE_KEY',
... ... @@ -68,8 +84,22 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
68 84 [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'],
69 85 [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'],
70 86 [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'],
71   - [ImportEntityColumnType.entityField, 'import.column-type.entity-field'],
72 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 103 [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
74 104 [ImportEntityColumnType.description, 'import.column-type.description'],
75 105 [ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'],
... ... @@ -85,6 +115,28 @@ export interface CsvColumnParam {
85 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 140 export interface FileType {
89 141 mimeType: string;
90 142 extension: string;
... ...
... ... @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22 22 import { ActionNotificationShow } from '@core/notification/notification.actions';
23 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 25 import { WINDOW } from '@core/services/window.service';
26 26 import { DOCUMENT } from '@angular/common';
27 27 import {
... ... @@ -44,7 +44,7 @@ import {
44 44 EntityAliasesDialogData
45 45 } from '@home/components/alias/entity-aliases-dialog.component';
46 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 48 import { EntityType } from '@shared/models/entity-type.models';
49 49 import { UtilsService } from '@core/services/utils.service';
50 50 import { WidgetService } from '@core/http/widget.service';
... ... @@ -59,6 +59,9 @@ import { DeviceProfileService } from '@core/http/device-profile.service';
59 59 import { DeviceProfile } from '@shared/models/device.models';
60 60 import { TenantProfile } from '@shared/models/tenant.model';
61 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 66 // @dynamic
64 67 @Injectable()
... ... @@ -75,6 +78,9 @@ export class ImportExportService {
75 78 private tenantProfileService: TenantProfileService,
76 79 private entityService: EntityService,
77 80 private ruleChainService: RuleChainService,
  81 + private deviceService: DeviceService,
  82 + private assetService: AssetService,
  83 + private edgeService: EdgeService,
78 84 private utils: UtilsService,
79 85 private itembuffer: ItemBufferService,
80 86 private dialog: MatDialog) {
... ... @@ -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 362 public importEntities(entitiesData: ImportEntityData[], entityType: EntityType, updateData: boolean,
346 363 importEntityCompleted?: () => void, config?: RequestConfig): Observable<ImportEntitiesResultInfo> {
347 364 let partSize = 100;
... ... @@ -563,6 +580,8 @@ export class ImportExportService {
563 580 if (isObject(obj2[key])) {
564 581 obj1[key] = obj1[key] || {};
565 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 585 } else {
567 586 obj1[key] = (obj1[key] || 0) + obj2[key];
568 587 }
... ...
... ... @@ -23,23 +23,28 @@
23 23 </mat-cell>
24 24 </ng-container>
25 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 27 <mat-cell *matCellDef="let column">
28 28 {{column.sampleData}}
29 29 </mat-cell>
30 30 </ng-container>
31 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 33 <mat-cell *matCellDef="let column">
34   - <mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
  34 + <mat-select [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
35 35 <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled">
36 36 {{ columnTypesTranslations.get(type.value) | translate }}
37 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 43 </mat-select>
39 44 </mat-cell>
40 45 </ng-container>
41 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 48 <mat-cell *matCellDef="let column">
44 49 <mat-form-field floatLabel="always" hideRequiredMarker
45 50 *ngIf="isColumnTypeDiffers(column.type)">
... ...
... ... @@ -14,14 +14,21 @@
14 14 * limitations under the License.
15 15 */
16 16 :host {
  17 + overflow-x: auto;
  18 +
17 19 .mat-column-order {
18 20 flex: 0 0 40px;
19 21 }
20 22 .mat-column-sampleData {
21 23 flex: 0 0 120px;
22 24 min-width: 120px;
  25 + max-width: 230px;
23 26 }
24 27 .mat-column-type {
  28 + flex: 0 0 180px;
  29 + min-width: 180px;
  30 + }
  31 + .mat-column-key {
25 32 flex: 0 0 120px;
26 33 min-width: 120px;
27 34 }
... ...
... ... @@ -57,8 +57,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
57 57
58 58 columnTypes: AssignmentColumnType[] = [];
59 59
  60 + columnDeviceCredentials: AssignmentColumnType[] = [];
  61 +
60 62 columnTypesTranslations = importEntityColumnTypeTranslations;
61 63
  64 + readonly entityTypeDevice = EntityType.DEVICE;
  65 +
62 66 private columns: CsvColumnParam[];
63 67
64 68 private valid = true;
... ... @@ -83,9 +87,26 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
83 87 { value: ImportEntityColumnType.sharedAttribute },
84 88 { value: ImportEntityColumnType.serverAttribute },
85 89 { value: ImportEntityColumnType.timeseries },
86   - { value: ImportEntityColumnType.accessToken },
87 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 110 break;
90 111 case EntityType.ASSET:
91 112 this.columnTypes.push(
... ... @@ -123,8 +144,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
123 144 const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1;
124 145 const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1;
125 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 147 const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1;
129 148 const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1;
130 149 const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1;
... ... @@ -139,14 +158,19 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
139 158 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel;
140 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 174 const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey);
151 175 if (edgeLicenseKeyColumnType) {
152 176 edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey;
... ...
... ... @@ -743,6 +743,14 @@ export interface DeviceCredentialMQTTBasic {
743 743 password: string;
744 744 }
745 745
  746 +export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
  747 + return {
  748 + clientId: '',
  749 + userName: '',
  750 + password: ''
  751 + };
  752 +}
  753 +
746 754 export interface DeviceSearchQuery extends EntitySearchQuery {
747 755 deviceTypes: Array<string>;
748 756 }
... ...
... ... @@ -17,6 +17,8 @@
17 17 import { EntityType } from '@shared/models/entity-type.models';
18 18 import { AttributeData } from './telemetry/telemetry.models';
19 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 23 export interface EntityInfo {
22 24 name?: string;
... ... @@ -32,12 +34,18 @@ export interface EntityInfoData {
32 34 }
33 35
34 36 export interface ImportEntityData {
  37 + lineNumber: number;
35 38 name: string;
36 39 type: string;
37 40 label: string;
38 41 gateway: boolean;
39 42 description: string;
40   - accessToken: string;
  43 + credential: {
  44 + accessToken?: string;
  45 + x509?: string;
  46 + mqtt?: DeviceCredentialMQTTBasic;
  47 + lwm2m?: Lwm2mSecurityConfigModels;
  48 + };
41 49 attributes: {
42 50 server: AttributeData[],
43 51 shared: AttributeData[]
... ... @@ -61,6 +69,7 @@ export interface ImportEntitiesResultInfo {
61 69 };
62 70 error?: {
63 71 entity: number;
  72 + errors?: string;
64 73 };
65 74 }
66 75
... ...
... ... @@ -58,6 +58,20 @@ export interface Lwm2mSecurityConfigModels {
58 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 75 export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig {
62 76 let security = {
63 77 securityConfigClientMode: securityConfigMode,
... ...
... ... @@ -2187,9 +2187,11 @@
2187 2187 "column-title": "Title",
2188 2188 "column-example": "Example value data",
2189 2189 "column-key": "Attribute/telemetry key",
  2190 + "credentials": "Credentials",
2190 2191 "csv-delimiter": "CSV delimiter",
2191 2192 "csv-first-line-header": "First line contains column names",
2192 2193 "csv-update-data": "Update attributes/telemetry",
  2194 + "details": "Details",
2193 2195 "import-csv-number-columns-error": "A file should contain at least two columns",
2194 2196 "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'",
2195 2197 "column-type": {
... ... @@ -2203,6 +2205,25 @@
2203 2205 "timeseries": "Timeseries",
2204 2206 "entity-field": "Entity field",
2205 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 2227 "isgateway": "Is Gateway",
2207 2228 "activity-time-from-gateway-device": "Activity time from gateway device",
2208 2229 "description": "Description",
... ...