Commit 8518b993a331117862622811f3246d701d3587fa
Committed by
GitHub
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; | ... | ... |
application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
0 → 100644
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 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
0 → 100644
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 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java
0 → 100644
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 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/importing/BulkImportColumnType.java
0 → 100644
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 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/importing/BulkImportRequest.java
0 → 100644
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 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java
0 → 100644
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 | } | ... | ... |
dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java
0 → 100644
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", | ... | ... |