diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql new file mode 100644 index 0000000000..e68392230a --- /dev/null +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -0,0 +1,66 @@ +-- +-- Copyright © 2016-2021 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS resource ( + id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid NOT NULL, + title varchar(255) NOT NULL, + resource_type varchar(32) NOT NULL, + resource_key varchar(255) NOT NULL, + search_text varchar(255), + file_name varchar(255) NOT NULL, + data varchar, + CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key) +); + +CREATE TABLE IF NOT EXISTS firmware ( + id uuid NOT NULL CONSTRAINT firmware_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid NOT NULL, + title varchar(255) NOT NULL, + version varchar(255) NOT NULL, + file_name varchar(255), + content_type varchar(255), + checksum_algorithm varchar(32), + checksum varchar(1020), + data bytea, + additional_info varchar, + search_text varchar(255), + CONSTRAINT firmware_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) +); + +ALTER TABLE device_profile + ADD COLUMN IF NOT EXISTS firmware_id uuid; + +ALTER TABLE device + ADD COLUMN IF NOT EXISTS firmware_id uuid; + +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_firmware_device_profile') THEN + ALTER TABLE device_profile + ADD CONSTRAINT fk_firmware_device_profile + FOREIGN KEY (firmware_id) REFERENCES firmware(id); + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_firmware_device') THEN + ALTER TABLE device + ADD CONSTRAINT fk_firmware_device + FOREIGN KEY (firmware_id) REFERENCES firmware(id); + END IF; + END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 04ea69d419..d9d9a232a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TbResourceInfo; @@ -61,6 +64,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; @@ -97,6 +101,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; @@ -114,6 +119,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.firmware.FirmwareStateService; import org.thingsboard.server.service.lwm2m.LwM2MModelsRepository; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.TbClusterService; @@ -232,6 +238,12 @@ public abstract class BaseController { @Autowired protected TbResourceService resourceService; + @Autowired + protected FirmwareService firmwareService; + + @Autowired + protected FirmwareStateService firmwareStateService; + @Autowired protected TbQueueProducerProvider producerProvider; @@ -470,6 +482,9 @@ public abstract class BaseController { case TB_RESOURCE: checkResourceId(new TbResourceId(entityId.getId()), operation); return; + case FIRMWARE: + checkFirmwareId(new FirmwareId(entityId.getId()), operation); + return; default: throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); } @@ -701,6 +716,30 @@ public abstract class BaseController { } } + Firmware checkFirmwareId(FirmwareId firmwareId, Operation operation) throws ThingsboardException { + try { + validateId(firmwareId, "Incorrect firmwareId " + firmwareId); + Firmware firmware = firmwareService.findFirmwareById(getCurrentUser().getTenantId(), firmwareId); + checkNotNull(firmware); + accessControlService.checkPermission(getCurrentUser(), Resource.FIRMWARE, operation, firmwareId, firmware); + return firmware; + } catch (Exception e) { + throw handleException(e, false); + } + } + + FirmwareInfo checkFirmwareInfoId(FirmwareId firmwareId, Operation operation) throws ThingsboardException { + try { + validateId(firmwareId, "Incorrect firmwareId " + firmwareId); + FirmwareInfo firmwareInfo = firmwareService.findFirmwareInfoById(getCurrentUser().getTenantId(), firmwareId); + checkNotNull(firmwareInfo); + accessControlService.checkPermission(getCurrentUser(), Resource.FIRMWARE, operation, firmwareId, firmwareInfo); + return firmwareInfo; + } catch (Exception e) { + throw handleException(e, false); + } + } + @SuppressWarnings("unchecked") protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); @@ -924,4 +963,11 @@ public abstract class BaseController { } } + protected MediaType parseMediaType(String contentType) { + try { + return MediaType.parseMediaType(contentType); + } catch (Exception e) { + return MediaType.APPLICATION_OCTET_STREAM; + } + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index c6409ebbad..0e4b758593 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -70,6 +70,7 @@ import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; @RestController @@ -117,13 +118,25 @@ public class DeviceController extends BaseController { checkEntity(device.getId(), device, Resource.DEVICE); + boolean created = device.getId() == null; + + boolean isFirmwareChanged = false; + + if (created) { + isFirmwareChanged = true; + } else { + Device oldDevice = deviceService.findDeviceById(getTenantId(), device.getId()); + if (!Objects.equals(device.getFirmwareId(), oldDevice.getFirmwareId()) || !oldDevice.getDeviceProfileId().equals(device.getDeviceProfileId())) { + isFirmwareChanged = true; + } + } + Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); tbClusterService.onDeviceChange(savedDevice, null); tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(), savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null); - tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), - device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); logEntityAction(savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), @@ -134,12 +147,19 @@ public class DeviceController extends BaseController { } else { deviceStateService.onDeviceUpdated(savedDevice); } + + if (isFirmwareChanged) { + firmwareStateService.update(savedDevice, created); + } + return savedDevice; - } catch (Exception e) { + } catch ( + Exception e) { logEntityAction(emptyId(EntityType.DEVICE), device, null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); throw handleException(e); } + } @PreAuthorize("hasAuthority('TENANT_ADMIN')") diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 81bf0ba1a7..c72ae1870d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -43,6 +43,7 @@ import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import java.util.Objects; import java.util.UUID; @RestController @@ -143,6 +144,15 @@ public class DeviceProfileController extends BaseController { checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE); + boolean isFirmwareChanged = false; + + if (!created) { + DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfile.getId()); + if (!Objects.equals(deviceProfile.getFirmwareId(), oldDeviceProfile.getFirmwareId())) { + isFirmwareChanged = true; + } + } + DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile)); tbClusterService.onDeviceProfileChange(savedDeviceProfile, null); @@ -153,6 +163,10 @@ public class DeviceProfileController extends BaseController { null, created ? ActionType.ADDED : ActionType.UPDATED, null); + if (isFirmwareChanged) { + firmwareStateService.update(savedDeviceProfile); + } + return savedDeviceProfile; } catch (Exception e) { logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile, diff --git a/application/src/main/java/org/thingsboard/server/controller/FirmwareController.java b/application/src/main/java/org/thingsboard/server/controller/FirmwareController.java new file mode 100644 index 0000000000..020a02b71c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/FirmwareController.java @@ -0,0 +1,195 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.nio.ByteBuffer; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class FirmwareController extends BaseController { + + public static final String FIRMWARE_ID = "firmwareId"; + + @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") + @RequestMapping(value = "/firmware/{firmwareId}/download", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity downloadFirmware(@PathVariable(FIRMWARE_ID) String strFirmwareId) throws ThingsboardException { + checkParameter(FIRMWARE_ID, strFirmwareId); + try { + FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId)); + Firmware firmware = checkFirmwareId(firmwareId, Operation.READ); + + ByteArrayResource resource = new ByteArrayResource(firmware.getData().array()); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + firmware.getFileName()) + .header("x-filename", firmware.getFileName()) + .contentLength(resource.contentLength()) + .contentType(parseMediaType(firmware.getContentType())) + .body(resource); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmware/info/{firmwareId}", method = RequestMethod.GET) + @ResponseBody + public FirmwareInfo getFirmwareInfoById(@PathVariable(FIRMWARE_ID) String strFirmwareId) throws ThingsboardException { + checkParameter(FIRMWARE_ID, strFirmwareId); + try { + FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId)); + return checkFirmwareInfoId(firmwareId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmware/{firmwareId}", method = RequestMethod.GET) + @ResponseBody + public Firmware getFirmwareById(@PathVariable(FIRMWARE_ID) String strFirmwareId) throws ThingsboardException { + checkParameter(FIRMWARE_ID, strFirmwareId); + try { + FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId)); + return checkFirmwareId(firmwareId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmware", method = RequestMethod.POST) + @ResponseBody + public FirmwareInfo saveFirmwareInfo(@RequestBody FirmwareInfo firmwareInfo) throws ThingsboardException { + firmwareInfo.setTenantId(getTenantId()); + checkEntity(firmwareInfo.getId(), firmwareInfo, Resource.FIRMWARE); + try { + return firmwareService.saveFirmwareInfo(firmwareInfo); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmware/{firmwareId}", method = RequestMethod.POST) + @ResponseBody + public Firmware saveFirmwareData(@PathVariable(FIRMWARE_ID) String strFirmwareId, + @RequestParam(required = false) String checksum, + @RequestParam(required = false) String checksumAlgorithm, + @RequestBody MultipartFile file) throws ThingsboardException { + checkParameter(FIRMWARE_ID, strFirmwareId); + try { + FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId)); + FirmwareInfo info = checkFirmwareInfoId(firmwareId, Operation.READ); + + Firmware firmware = new Firmware(firmwareId); + firmware.setCreatedTime(info.getCreatedTime()); + firmware.setTenantId(getTenantId()); + firmware.setTitle(info.getTitle()); + firmware.setVersion(info.getVersion()); + firmware.setAdditionalInfo(info.getAdditionalInfo()); + + if (StringUtils.isEmpty(checksumAlgorithm)) { + checksumAlgorithm = "sha256"; + checksum = Hashing.sha256().hashBytes(file.getBytes()).toString(); + } + + firmware.setChecksumAlgorithm(checksumAlgorithm); + firmware.setChecksum(checksum); + firmware.setFileName(file.getOriginalFilename()); + firmware.setContentType(file.getContentType()); + firmware.setData(ByteBuffer.wrap(file.getBytes())); + return firmwareService.saveFirmware(firmware); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmwares", method = RequestMethod.GET) + @ResponseBody + public PageData getFirmwares(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(firmwareService.findTenantFirmwaresByTenantId(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmwares/{hasData}", method = RequestMethod.GET) + @ResponseBody + public PageData getFirmwares(@PathVariable("hasData") boolean hasData, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(firmwareService.findTenantFirmwaresByTenantIdAndHasData(getTenantId(), hasData, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/firmware/{firmwareId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteResource(@PathVariable("firmwareId") String strFirmwareId) throws ThingsboardException { + checkParameter(FIRMWARE_ID, strFirmwareId); + try { + FirmwareId firmwareId = new FirmwareId(toUUID(strFirmwareId)); + checkFirmwareInfoId(firmwareId, Operation.DELETE); + firmwareService.deleteFirmware(getTenantId(), firmwareId); + } catch (Exception e) { + throw handleException(e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 0190c3e790..adba9827cd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -55,12 +55,6 @@ public class TbResourceController extends BaseController { public static final String RESOURCE_ID = "resourceId"; - private final TbResourceService resourceService; - - public TbResourceController(TbResourceService resourceService) { - this.resourceService = resourceService; - } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET) @ResponseBody @@ -187,7 +181,7 @@ public class TbResourceController extends BaseController { @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.DELETE) @ResponseBody public void deleteResource(@PathVariable("resourceId") String strResourceId) throws ThingsboardException { - checkParameter("resourceId", strResourceId); + checkParameter(RESOURCE_ID, strResourceId); try { TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); TbResource tbResource = checkResourceId(resourceId, Operation.DELETE); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 4a9a110fab..a22a22d240 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -230,7 +230,6 @@ public class ThingsboardInstallService { systemDataLoaderService.createAdminSettings(); systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.createOAuth2Templates(); - systemDataLoaderService.loadSystemLwm2mResources(); // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); diff --git a/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java b/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java new file mode 100644 index 0000000000..3e03dd99e0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/firmware/DefaultFirmwareStateService.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.firmware; + +import com.google.common.util.concurrent.FutureCallback; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.firmware.FirmwareService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static org.thingsboard.server.common.data.DataConstants.FIRMWARE_CHECKSUM; +import static org.thingsboard.server.common.data.DataConstants.FIRMWARE_CHECKSUM_ALGORITHM; +import static org.thingsboard.server.common.data.DataConstants.FIRMWARE_SIZE; +import static org.thingsboard.server.common.data.DataConstants.FIRMWARE_TITLE; +import static org.thingsboard.server.common.data.DataConstants.FIRMWARE_VERSION; + +@Slf4j +@Service +@TbCoreComponent +public class DefaultFirmwareStateService implements FirmwareStateService { + + private final FirmwareService firmwareService; + private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; + private final RuleEngineTelemetryService telemetryService; + + public DefaultFirmwareStateService(FirmwareService firmwareService, DeviceService deviceService, DeviceProfileService deviceProfileService, RuleEngineTelemetryService telemetryService) { + this.firmwareService = firmwareService; + this.deviceService = deviceService; + this.deviceProfileService = deviceProfileService; + this.telemetryService = telemetryService; + } + + @Override + public void update(Device device, boolean created) { + FirmwareId firmwareId = device.getFirmwareId(); + if (firmwareId == null) { + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + firmwareId = deviceProfile.getFirmwareId(); + } + + if (firmwareId == null) { + if (!created) { + remove(device); + } + } else { + update(device, firmwareService.findFirmwareById(device.getTenantId(), firmwareId), System.currentTimeMillis()); + } + } + + @Override + public void update(DeviceProfile deviceProfile) { + TenantId tenantId = deviceProfile.getTenantId(); + + Consumer updateConsumer; + if (deviceProfile.getFirmwareId() != null) { + Firmware firmware = firmwareService.findFirmwareById(tenantId, deviceProfile.getFirmwareId()); + long ts = System.currentTimeMillis(); + updateConsumer = d -> update(d, firmware, ts); + } else { + updateConsumer = this::remove; + } + + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + //TODO: create a query which will return devices without firmware + pageData = deviceService.findDevicesByTenantIdAndType(tenantId, deviceProfile.getName(), pageLink); + + pageData.getData().stream().filter(d -> d.getFirmwareId() == null).forEach(updateConsumer); + + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + } + + private void update(Device device, Firmware firmware, long ts) { + TenantId tenantId = device.getTenantId(); + DeviceId deviceId = device.getId(); + + List telemetry = new ArrayList<>(); + telemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(DataConstants.TARGET_FIRMWARE_TITLE, firmware.getTitle()))); + telemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(DataConstants.TARGET_FIRMWARE_VERSION, firmware.getVersion()))); + + telemetryService.saveAndNotify(tenantId, deviceId, telemetry, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void tmp) { + log.trace("[{}] Success save telemetry with target firmware for device!", deviceId); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to save telemetry with target firmware for device!", deviceId, t); + } + }); + + List attributes = new ArrayList<>(); + + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(DataConstants.FIRMWARE_TITLE, firmware.getTitle()))); + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(DataConstants.FIRMWARE_VERSION, firmware.getVersion()))); + + attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(FIRMWARE_SIZE, (long) firmware.getData().array().length))); + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(DataConstants.FIRMWARE_CHECKSUM_ALGORITHM, firmware.getChecksumAlgorithm()))); + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(DataConstants.FIRMWARE_CHECKSUM, firmware.getChecksum()))); + telemetryService.saveAndNotify(tenantId, deviceId, DataConstants.SHARED_SCOPE, attributes, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void tmp) { + log.trace("[{}] Success save attributes with target firmware!", deviceId); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to save attributes with target firmware!", deviceId, t); + } + }); + } + + private void remove(Device device) { + telemetryService.deleteAndNotify(device.getTenantId(), device.getId(), DataConstants.SHARED_SCOPE, + Arrays.asList(FIRMWARE_TITLE, FIRMWARE_VERSION, FIRMWARE_SIZE, FIRMWARE_CHECKSUM_ALGORITHM, FIRMWARE_CHECKSUM), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable Void tmp) { + log.trace("[{}] Success remove target firmware attributes!", device.getId()); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to remove target firmware attributes!", device.getId(), t); + } + }); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/firmware/FirmwareStateService.java b/application/src/main/java/org/thingsboard/server/service/firmware/FirmwareStateService.java new file mode 100644 index 0000000000..21dc7d8696 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/firmware/FirmwareStateService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.firmware; + +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; + +public interface FirmwareStateService { + + void update(Device device, boolean created); + + void update(DeviceProfile deviceProfile); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 6f257a367e..b49819b7ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -58,11 +58,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; -import org.thingsboard.server.common.data.query.EntityKey; -import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -445,11 +442,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.loadSystemWidgets(); } - @Override - public void loadSystemLwm2mResources() throws Exception { - installScripts.loadSystemLwm2mResources(); - } - private User createUser(Authority authority, TenantId tenantId, CustomerId customerId, diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index fb7634118b..f36a8f84ce 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -22,8 +22,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.ResourceType; -import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,7 +31,6 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -45,7 +42,6 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Base64; import java.util.Optional; import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; @@ -196,29 +192,6 @@ public class InstallScripts { } } - public void loadSystemLwm2mResources() throws Exception { - Path modelsDir = Paths.get(getDataDir(), MODELS_DIR); - if (Files.isDirectory(modelsDir)) { - try (DirectoryStream dirStream = Files.newDirectoryStream(modelsDir, path -> path.toString().endsWith(XML_EXT))) { - dirStream.forEach( - path -> { - try { - byte[] fileBytes = Files.readAllBytes(path); - TbResource resource = new TbResource(); - resource.setFileName(path.getFileName().toString()); - resource.setTenantId(TenantId.SYS_TENANT_ID); - resource.setResourceType(ResourceType.LWM2M_MODEL); - resource.setData(Base64.getEncoder().encodeToString(fileBytes)); - resourceService.saveResource(resource); - } catch (Exception e) { - throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", path.toString())); - } - } - ); - } - } - } - public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 6b946bad52..922db4c2b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -449,26 +449,12 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService case "3.2.2": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Updating schema ..."); - try { - conn.createStatement().execute("CREATE TABLE IF NOT EXISTS resource ( " + - "id uuid NOT NULL CONSTRAINT resource_pkey PRIMARY KEY, " + - "created_time bigint NOT NULL, " + - "tenant_id uuid NOT NULL, " + - "title varchar(255) NOT NULL, " + - "resource_type varchar(32) NOT NULL, " + - "resource_key varchar(255) NOT NULL, " + - "search_text varchar(255), " + - "file_name varchar(255) NOT NULL, " + - "data varchar, " + - "CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key)" + - ");"); - - conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); - installScripts.loadSystemLwm2mResources(); - } catch (Exception e) { - log.error("Failed updating schema!!!", e); - } + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); } break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index a6f33f476f..73e2b6ea57 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -33,6 +33,4 @@ public interface SystemDataLoaderService { void deleteSystemWidgetBundle(String bundleAlias) throws Exception; - void loadSystemLwm2mResources() throws Exception; - } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index ba53ceea98..b6ac71169e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -37,7 +37,8 @@ public enum Resource { TENANT_PROFILE(EntityType.TENANT_PROFILE), DEVICE_PROFILE(EntityType.DEVICE_PROFILE), API_USAGE_STATE(EntityType.API_USAGE_STATE), - TB_RESOURCE(EntityType.TB_RESOURCE); + TB_RESOURCE(EntityType.TB_RESOURCE), + FIRMWARE(EntityType.FIRMWARE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index c36a70821f..a6fce632b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -42,6 +42,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); put(Resource.TB_RESOURCE, tbResourcePermissionChecker); + put(Resource.FIRMWARE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 9613d708d2..768eb592f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -23,16 +23,19 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.cache.firmware.FirmwareCacheWriter; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.Firmware; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData; @@ -40,6 +43,7 @@ import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileC import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -55,6 +59,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; +import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.TbResourceService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -66,7 +71,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFro import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetResourceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; @@ -109,6 +113,8 @@ public class DefaultTransportApiService implements TransportApiService { private final DataDecodingEncodingService dataDecodingEncodingService; private final DeviceProvisionService deviceProvisionService; private final TbResourceService resourceService; + private final FirmwareService firmwareService; + private final FirmwareCacheWriter firmwareCacheWriter; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -117,7 +123,7 @@ public class DefaultTransportApiService implements TransportApiService { RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, - DeviceProvisionService deviceProvisionService, TbResourceService resourceService) { + DeviceProvisionService deviceProvisionService, TbResourceService resourceService, FirmwareService firmwareService, CacheManager cacheManager, FirmwareCacheWriter firmwareCacheWriter) { this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.apiUsageStateService = apiUsageStateService; @@ -130,6 +136,8 @@ public class DefaultTransportApiService implements TransportApiService { this.dataDecodingEncodingService = dataDecodingEncodingService; this.deviceProvisionService = deviceProvisionService; this.resourceService = resourceService; + this.firmwareService = firmwareService; + this.firmwareCacheWriter = firmwareCacheWriter; } @Override @@ -166,6 +174,9 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasResourceRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getResourceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasFirmwareRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getFirmwareRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); @@ -313,14 +324,14 @@ public class DefaultTransportApiService implements TransportApiService { } catch (ProvisionFailedException e) { return Futures.immediateFuture(getTransportApiResponseMsg( new DeviceCredentials(), - TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage()))); + TransportProtos.ResponseStatus.valueOf(e.getMessage()))); } - return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ProvisionResponseStatus.SUCCESS), + return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ResponseStatus.SUCCESS), dbCallbackExecutorService); } - private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials deviceCredentials, TransportProtos.ProvisionResponseStatus status) { - if (!status.equals(ProvisionResponseStatus.SUCCESS)) { + private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials deviceCredentials, TransportProtos.ResponseStatus status) { + if (!status.equals(TransportProtos.ResponseStatus.SUCCESS)) { return TransportApiResponseMsg.newBuilder().setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder().setStatus(status).build()).build(); } TransportProtos.ProvisionDeviceResponseMsg.Builder provisionResponse = TransportProtos.ProvisionDeviceResponseMsg.newBuilder() @@ -438,6 +449,46 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture handle(TransportProtos.GetFirmwareRequestMsg requestMsg) { + TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); + DeviceId deviceId = new DeviceId(new UUID(requestMsg.getDeviceIdMSB(), requestMsg.getDeviceIdLSB())); + Device device = deviceService.findDeviceById(tenantId, deviceId); + + if (device == null) { + return getEmptyTransportApiResponseFuture(); + } + + FirmwareId firmwareId = device.getFirmwareId(); + + if (firmwareId == null) { + firmwareId = deviceProfileCache.find(device.getDeviceProfileId()).getFirmwareId(); + } + + TransportProtos.GetFirmwareResponseMsg.Builder builder = TransportProtos.GetFirmwareResponseMsg.newBuilder(); + + if (firmwareId == null) { + builder.setResponseStatus(TransportProtos.ResponseStatus.NOT_FOUND); + } else { + Firmware firmware = firmwareService.findFirmwareById(tenantId, firmwareId); + + if (firmware == null) { + builder.setResponseStatus(TransportProtos.ResponseStatus.NOT_FOUND); + } else { + builder.setResponseStatus(TransportProtos.ResponseStatus.SUCCESS); + builder.setFirmwareIdMSB(firmwareId.getId().getMostSignificantBits()); + builder.setFirmwareIdLSB(firmwareId.getId().getLeastSignificantBits()); + builder.setFileName(firmware.getFileName()); + builder.setContentType(firmware.getContentType()); + firmwareCacheWriter.put(firmwareId.toString(), firmware.getData().array()); + } + } + + return Futures.immediateFuture( + TransportApiResponseMsg.newBuilder() + .setFirmwareResponseMsg(builder.build()) + .build()); + } + private ListenableFuture handleRegistration(TransportProtos.LwM2MRegistrationRequestMsg msg) { TenantId tenantId = new TenantId(UUID.fromString(msg.getTenantId())); String deviceName = msg.getEndpoint(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 1e09461a7f..a493e1ad48 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -281,7 +281,7 @@ actors: tenant: create_components_on_init: "${ACTORS_TENANT_CREATE_COMPONENTS_ON_INIT:true}" session: - max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:1}" + max_concurrent_sessions_per_device: "${ACTORS_MAX_CONCURRENT_SESSION_PER_DEVICE:2}" sync: # Default timeout for processing request using synchronous session (HTTP, CoAP) in milliseconds timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}" @@ -365,6 +365,9 @@ caffeine: tokensOutdatageTime: timeToLiveInMinutes: 20000 maxSize: 10000 + firmwares: + timeToLiveInMinutes: 1440 + maxSize: 100 redis: # standalone or cluster @@ -437,6 +440,9 @@ spring.resources.chain: content: enabled: "true" +spring.servlet.multipart.max-file-size: "50MB" +spring.servlet.multipart.max-request-size: "50MB" + spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" spring.jpa.properties.hibernate.order_by.default_null_ordering: "last" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseFirmwareControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseFirmwareControllerTest.java new file mode 100644 index 0000000000..425a21e1a2 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseFirmwareControllerTest.java @@ -0,0 +1,302 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseFirmwareControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + + public static final String TITLE = "My firmware"; + private static final String FILE_NAME = "filename.txt"; + private static final String VERSION = "v1.0"; + private static final String CONTENT_TYPE = "text/plain"; + private static final String CHECKSUM_ALGORITHM = "sha256"; + private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; + private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{1}); + + private Tenant savedTenant; + private User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveFirmware() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + Assert.assertNotNull(savedFirmwareInfo); + Assert.assertNotNull(savedFirmwareInfo.getId()); + Assert.assertTrue(savedFirmwareInfo.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedFirmwareInfo.getTenantId()); + Assert.assertEquals(firmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); + Assert.assertEquals(firmwareInfo.getVersion(), savedFirmwareInfo.getVersion()); + + savedFirmwareInfo.setAdditionalInfo(JacksonUtil.newObjectNode()); + + save(savedFirmwareInfo); + + FirmwareInfo foundFirmwareInfo = doGet("/api/firmware/info/" + savedFirmwareInfo.getId().getId().toString(), FirmwareInfo.class); + Assert.assertEquals(foundFirmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); + } + + @Test + public void testSaveFirmwareData() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + Assert.assertNotNull(savedFirmwareInfo); + Assert.assertNotNull(savedFirmwareInfo.getId()); + Assert.assertTrue(savedFirmwareInfo.getCreatedTime() > 0); + Assert.assertEquals(savedTenant.getId(), savedFirmwareInfo.getTenantId()); + Assert.assertEquals(firmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); + Assert.assertEquals(firmwareInfo.getVersion(), savedFirmwareInfo.getVersion()); + + savedFirmwareInfo.setAdditionalInfo(JacksonUtil.newObjectNode()); + + save(savedFirmwareInfo); + + FirmwareInfo foundFirmwareInfo = doGet("/api/firmware/info/" + savedFirmwareInfo.getId().getId().toString(), FirmwareInfo.class); + Assert.assertEquals(foundFirmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); + + MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); + + Firmware savedFirmware = savaData("/api/firmware/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + + Assert.assertEquals(FILE_NAME, savedFirmware.getFileName()); + Assert.assertEquals(CONTENT_TYPE, savedFirmware.getContentType()); + } + + @Test + public void testUpdateFirmwareFromDifferentTenant() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + loginDifferentTenant(); + doPost("/api/firmware", savedFirmwareInfo, FirmwareInfo.class, status().isForbidden()); + deleteDifferentTenant(); + } + + @Test + public void testFindFirmwareInfoById() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + FirmwareInfo foundFirmware = doGet("/api/firmware/info/" + savedFirmwareInfo.getId().getId().toString(), FirmwareInfo.class); + Assert.assertNotNull(foundFirmware); + Assert.assertEquals(savedFirmwareInfo, foundFirmware); + } + + @Test + public void testFindFirmwareById() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); + + Firmware savedFirmware = savaData("/api/firmware/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + + Firmware foundFirmware = doGet("/api/firmware/" + savedFirmwareInfo.getId().getId().toString(), Firmware.class); + Assert.assertNotNull(foundFirmware); + Assert.assertEquals(savedFirmware, foundFirmware); + } + + @Test + public void testDeleteFirmware() throws Exception { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + doDelete("/api/firmware/" + savedFirmwareInfo.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/firmware/info/" + savedFirmwareInfo.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testFindTenantFirmwares() throws Exception { + List firmwares = new ArrayList<>(); + for (int i = 0; i < 165; i++) { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION + i); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + if (i > 100) { + MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); + + Firmware savedFirmware = savaData("/api/firmware/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + firmwares.add(new FirmwareInfo(savedFirmware)); + } else { + firmwares.add(savedFirmwareInfo); + } + } + + List loadedFirmwares = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/firmwares?", + new TypeReference<>() { + }, pageLink); + loadedFirmwares.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(firmwares, idComparator); + Collections.sort(loadedFirmwares, idComparator); + + Assert.assertEquals(firmwares, loadedFirmwares); + } + + @Test + public void testFindTenantFirmwaresByHasData() throws Exception { + List firmwaresWithData = new ArrayList<>(); + List firmwaresWithoutData = new ArrayList<>(); + + for (int i = 0; i < 165; i++) { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION + i); + + FirmwareInfo savedFirmwareInfo = save(firmwareInfo); + + if (i > 100) { + MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); + + Firmware savedFirmware = savaData("/api/firmware/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); + firmwaresWithData.add(new FirmwareInfo(savedFirmware)); + } else { + firmwaresWithoutData.add(savedFirmwareInfo); + } + } + + List loadedFirmwaresWithData = new ArrayList<>(); + PageLink pageLink = new PageLink(24); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/firmwares/true?", + new TypeReference<>() { + }, pageLink); + loadedFirmwaresWithData.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + List loadedFirmwaresWithoutData = new ArrayList<>(); + pageLink = new PageLink(24); + do { + pageData = doGetTypedWithPageLink("/api/firmwares/false?", + new TypeReference<>() { + }, pageLink); + loadedFirmwaresWithoutData.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(firmwaresWithData, idComparator); + Collections.sort(firmwaresWithoutData, idComparator); + Collections.sort(loadedFirmwaresWithData, idComparator); + Collections.sort(loadedFirmwaresWithoutData, idComparator); + + Assert.assertEquals(firmwaresWithData, loadedFirmwaresWithData); + Assert.assertEquals(firmwaresWithoutData, loadedFirmwaresWithoutData); + } + + + private FirmwareInfo save(FirmwareInfo firmwareInfo) throws Exception { + return doPost("/api/firmware", firmwareInfo, FirmwareInfo.class); + } + + protected Firmware savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception { + MockMultipartHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.multipart(urlTemplate, params); + postRequest.file(content); + setJwtToken(postRequest); + return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), Firmware.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/FirmwareControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/FirmwareControllerSqlTest.java new file mode 100644 index 0000000000..a0e4a838ca --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/FirmwareControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseFirmwareControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class FirmwareControllerSqlTest extends BaseFirmwareControllerTest { +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/AbstractRedisFirmwareCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/AbstractRedisFirmwareCache.java new file mode 100644 index 0000000000..371b85b94d --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/AbstractRedisFirmwareCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +import org.springframework.data.redis.connection.RedisConnectionFactory; + +import static org.thingsboard.server.common.data.CacheConstants.FIRMWARE_CACHE; + +public abstract class AbstractRedisFirmwareCache { + + protected final RedisConnectionFactory redisConnectionFactory; + + protected AbstractRedisFirmwareCache(RedisConnectionFactory redisConnectionFactory) { + this.redisConnectionFactory = redisConnectionFactory; + } + + protected byte[] toFirmwareCacheKey(String key) { + return String.format("%s::%s", FIRMWARE_CACHE, key).getBytes(); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheReader.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheReader.java new file mode 100644 index 0000000000..f7e05f818c --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheReader.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import static org.thingsboard.server.common.data.CacheConstants.FIRMWARE_CACHE; + +@Service +@ConditionalOnExpression("(('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport') && ('${cache.type:null}'=='caffeine' || '${cache.type:null}'=='caffeine')") +public class CaffeineFirmwareCacheReader implements FirmwareCacheReader { + + private final CacheManager cacheManager; + + public CaffeineFirmwareCacheReader(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public byte[] get(String key) { + return get(key, 0, 0); + } + + @Override + public byte[] get(String key, int chunkSize, int chunk) { + byte[] data = cacheManager.getCache(FIRMWARE_CACHE).get(key, byte[].class); + + if (chunkSize < 1) { + return data; + } + + if (data != null && data.length > 0) { + int startIndex = chunkSize * chunk; + + int size = Math.min(data.length - startIndex, chunkSize); + + if (startIndex < data.length && size > 0) { + byte[] result = new byte[size]; + System.arraycopy(data, startIndex, result, 0, size); + return result; + } + } + return new byte[0]; + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheWriter.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheWriter.java new file mode 100644 index 0000000000..e543739382 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/CaffeineFirmwareCacheWriter.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; + +import static org.thingsboard.server.common.data.CacheConstants.FIRMWARE_CACHE; + +@Service +@ConditionalOnExpression("(('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='core') && ('${cache.type:null}'=='caffeine' || '${cache.type:null}'=='caffeine')") +public class CaffeineFirmwareCacheWriter implements FirmwareCacheWriter { + + private final CacheManager cacheManager; + + public CaffeineFirmwareCacheWriter(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public void put(String key, byte[] value) { + cacheManager.getCache(FIRMWARE_CACHE).putIfAbsent(key, value); + } +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheReader.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheReader.java new file mode 100644 index 0000000000..4b4dab4caa --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheReader.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +public interface FirmwareCacheReader { + byte[] get(String key); + + byte[] get(String key, int chunkSize, int chunk); +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheWriter.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheWriter.java new file mode 100644 index 0000000000..f1387f9ff1 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/FirmwareCacheWriter.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +public interface FirmwareCacheWriter { + void put(String key, byte[] value); +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheReader.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheReader.java new file mode 100644 index 0000000000..9bf8b40230 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheReader.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnExpression("(('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport') && '${cache.type:null}'=='redis'") +public class RedisFirmwareCacheReader extends AbstractRedisFirmwareCache implements FirmwareCacheReader { + + public RedisFirmwareCacheReader(RedisConnectionFactory redisConnectionFactory) { + super(redisConnectionFactory); + } + + @Override + public byte[] get(String key) { + return get(key, 0, 0); + } + + @Override + public byte[] get(String key, int chunkSize, int chunk) { + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + if (chunkSize == 0) { + return connection.get(toFirmwareCacheKey(key)); + } + + int startIndex = chunkSize * chunk; + int endIndex = startIndex + chunkSize - 1; + return connection.getRange(toFirmwareCacheKey(key), startIndex, endIndex); + } + } + +} diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheWriter.java b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheWriter.java new file mode 100644 index 0000000000..6ba3ee91f6 --- /dev/null +++ b/common/cache/src/main/java/org/thingsboard/server/cache/firmware/RedisFirmwareCacheWriter.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.cache.firmware; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnExpression("(('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='core') && '${cache.type:null}'=='redis'") +public class RedisFirmwareCacheWriter extends AbstractRedisFirmwareCache implements FirmwareCacheWriter { + + public RedisFirmwareCacheWriter(RedisConnectionFactory redisConnectionFactory) { + super(redisConnectionFactory); + } + + @Override + public void put(String key, byte[] value) { + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + connection.set(toFirmwareCacheKey(key), value); + } + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/firmware/FirmwareService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/firmware/FirmwareService.java new file mode 100644 index 0000000000..0fd26fee6e --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/firmware/FirmwareService.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.firmware; + +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; + +public interface FirmwareService { + + FirmwareInfo saveFirmwareInfo(FirmwareInfo firmwareInfo); + + Firmware saveFirmware(Firmware firmware); + + Firmware findFirmwareById(TenantId tenantId, FirmwareId firmwareId); + + FirmwareInfo findFirmwareInfoById(TenantId tenantId, FirmwareId firmwareId); + + PageData findTenantFirmwaresByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findTenantFirmwaresByTenantIdAndHasData(TenantId tenantId, boolean hasData, PageLink pageLink); + + void deleteFirmware(TenantId tenantId, FirmwareId firmwareId); + + void deleteFirmwaresByTenantId(TenantId tenantId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java index 0093b43dfe..d879827008 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceService.java @@ -32,7 +32,7 @@ import java.util.List; public interface TbResourceService { TbResource saveResource(TbResource resource) throws InvalidDDFFileException, IOException; - TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceKey); TbResource findResourceById(TenantId tenantId, TbResourceId resourceId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 62f625f285..ba14b24292 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -28,4 +28,5 @@ public class CacheConstants { public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime"; + public static final String FIRMWARE_CACHE = "firmwares"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 39fa30b07f..4f19e1589f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -91,4 +91,19 @@ public class DataConstants { public static final String USERNAME = "username"; public static final String PASSWORD = "password"; + //firmware + //telemetry + public static final String CURRENT_FIRMWARE_TITLE = "cur_fw_title"; + public static final String CURRENT_FIRMWARE_VERSION = "cur_fw_version"; + public static final String TARGET_FIRMWARE_TITLE = "target_fw_title"; + public static final String TARGET_FIRMWARE_VERSION = "target_fw_version"; + public static final String CURRENT_FIRMWARE_STATE = "cur_fw_state"; + + //attributes + //telemetry + public static final String FIRMWARE_TITLE = "fw_title"; + public static final String FIRMWARE_VERSION = "fw_version"; + public static final String FIRMWARE_SIZE = "fw_size"; + public static final String FIRMWARE_CHECKSUM = "fw_checksum"; + public static final String FIRMWARE_CHECKSUM_ALGORITHM = "fw_checksum_algorithm"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 2b5f9a9c1d..5282435c0a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.NoXss; @@ -48,6 +49,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen @JsonIgnore private byte[] deviceDataBytes; + private FirmwareId firmwareId; + public Device() { super(); } @@ -65,6 +68,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.label = device.getLabel(); this.deviceProfileId = device.getDeviceProfileId(); this.setDeviceData(device.getDeviceData()); + this.firmwareId = device.getFirmwareId(); } public Device updateDevice(Device device) { @@ -159,6 +163,14 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen return getName(); } + public FirmwareId getFirmwareId() { + return firmwareId; + } + + public void setFirmwareId(FirmwareId firmwareId) { + this.firmwareId = firmwareId; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -175,6 +187,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen builder.append(", deviceProfileId="); builder.append(deviceProfileId); builder.append(", deviceData="); + builder.append(firmwareId); + builder.append(", firmwareId="); builder.append(deviceData); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 44c1c4b0ac..660d9dec93 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.NoXss; @@ -56,6 +57,8 @@ public class DeviceProfile extends SearchTextBased implements H @NoXss private String provisionDeviceKey; + private FirmwareId firmwareId; + public DeviceProfile() { super(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index f289f802d8..36be053789 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, FIRMWARE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Firmware.java b/common/data/src/main/java/org/thingsboard/server/common/data/Firmware.java new file mode 100644 index 0000000000..6b8f48c5e8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Firmware.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.FirmwareId; + +import java.nio.ByteBuffer; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Firmware extends FirmwareInfo { + + private static final long serialVersionUID = 3091601761339422546L; + + private String fileName; + + private String contentType; + + private String checksumAlgorithm; + + private String checksum; + + private transient ByteBuffer data; + + public Firmware() { + super(); + } + + public Firmware(FirmwareId id) { + super(id); + } + + public Firmware(Firmware firmware) { + super(firmware); + this.fileName = firmware.getFileName(); + this.contentType = firmware.getContentType(); + this.data = firmware.getData(); + this.checksumAlgorithm = firmware.getChecksumAlgorithm(); + this.checksum = firmware.getChecksum(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/FirmwareInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/FirmwareInfo.java new file mode 100644 index 0000000000..224e94b59c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/FirmwareInfo.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; + +@Slf4j +@Data +@EqualsAndHashCode(callSuper = true) +public class FirmwareInfo extends SearchTextBasedWithAdditionalInfo implements HasTenantId { + + private static final long serialVersionUID = 3168391583570815419L; + + private TenantId tenantId; + private String title; + private String version; + private boolean hasData; + + public FirmwareInfo() { + super(); + } + + public FirmwareInfo(FirmwareId id) { + super(id); + } + + public FirmwareInfo(FirmwareInfo firmwareInfo) { + super(firmwareInfo); + this.tenantId = firmwareInfo.getTenantId(); + this.title = firmwareInfo.getTitle(); + this.version = firmwareInfo.getVersion(); + this.hasData = firmwareInfo.isHasData(); + } + + @Override + public String getSearchText() { + return title; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index d72982aeca..7fa869e14f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -26,6 +26,8 @@ import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) public class TbResourceInfo extends SearchTextBased implements HasTenantId { + private static final long serialVersionUID = 7282664529021651736L; + private TenantId tenantId; private String title; private ResourceType resourceType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java index f736fe371f..419f9c7f43 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -30,6 +30,9 @@ public class MqttTopics { private static final String CLAIM = "/claim"; private static final String SUB_TOPIC = "+"; private static final String PROVISION = "/provision"; + private static final String FIRMWARE = "/fw"; + private static final String CHUNK = "/chunk/"; + private static final String ERROR = "/error"; private static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE; private static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST; @@ -69,6 +72,13 @@ public class MqttTopics { public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_REQUEST; public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_RESPONSE; + // v2 topics + public static final String BASE_DEVICE_API_TOPIC_V2 = "v2"; + + public static final String DEVICE_FIRMWARE_RESPONSE_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + RESPONSE + "/"; + public static final String DEVICE_FIRMWARE_RESPONSES_TOPIC = DEVICE_FIRMWARE_RESPONSE_TOPIC_PREFIX + SUB_TOPIC + CHUNK + SUB_TOPIC; + public static final String DEVICE_FIRMWARE_ERROR_TOPIC = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + ERROR; + private MqttTopics() { } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 8e52a5b2a7..9179984a12 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -70,6 +70,8 @@ public class EntityIdFactory { return new ApiUsageStateId(uuid); case TB_RESOURCE: return new TbResourceId(uuid); + case FIRMWARE: + return new FirmwareId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/FirmwareId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/FirmwareId.java new file mode 100644 index 0000000000..3cdee53f58 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/FirmwareId.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class FirmwareId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public FirmwareId(@JsonProperty("id") UUID id) { + super(id); + } + + public static FirmwareId fromString(String firmwareId) { + return new FirmwareId(UUID.fromString(firmwareId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.FIRMWARE; + } + +} diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 7e9ff076f2..7abcd3613f 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -336,17 +336,33 @@ message ProvisionDeviceCredentialsMsg { } message ProvisionDeviceResponseMsg { - ProvisionResponseStatus status = 1; + ResponseStatus status = 1; CredentialsType credentialsType = 2; string credentialsValue = 3; } -enum ProvisionResponseStatus { +enum ResponseStatus { UNKNOWN = 0; SUCCESS = 1; NOT_FOUND = 2; FAILURE = 3; } + +message GetFirmwareRequestMsg { + int64 deviceIdMSB = 1; + int64 deviceIdLSB = 2; + int64 tenantIdMSB = 3; + int64 tenantIdLSB = 4; +} + +message GetFirmwareResponseMsg { + ResponseStatus responseStatus = 1; + int64 firmwareIdMSB = 2; + int64 firmwareIdLSB = 3; + string contentType = 4; + string fileName = 5; +} + //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. message SubscriptionInfoProto { int64 lastActivityTime = 1; @@ -552,6 +568,7 @@ message TransportApiRequestMsg { ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 8; GetResourceRequestMsg resourceRequestMsg = 9; + GetFirmwareRequestMsg firmwareRequestMsg = 10; } /* Response from ThingsBoard Core Service to Transport Service */ @@ -562,6 +579,7 @@ message TransportApiResponseMsg { ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; LwM2MResponseMsg lwM2MResponseMsg = 6; GetResourceResponseMsg resourceResponseMsg = 7; + GetFirmwareResponseMsg firmwareResponseMsg = 8; } /* Messages that are handled by ThingsBoard Core Service */ diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 074be3ae47..cacf6cfc27 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -398,7 +398,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { @Override public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg msg) { CoAP.ResponseCode responseCode = CoAP.ResponseCode.CREATED; - if (!msg.getStatus().equals(TransportProtos.ProvisionResponseStatus.SUCCESS)) { + if (!msg.getStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) { responseCode = CoAP.ResponseCode.BAD_REQUEST; } if (payloadType.equals(TransportPayloadType.JSON)) { diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index e222b690ef..5c10202d00 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -20,7 +20,10 @@ import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; @@ -41,7 +44,6 @@ import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; @@ -53,7 +55,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMs import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import javax.servlet.http.HttpServletRequest; @@ -204,6 +205,23 @@ public class DeviceApiController { return responseWriter; } + @RequestMapping(value = "/{deviceToken}/firmware", method = RequestMethod.GET) + public DeferredResult getFirmware(@PathVariable("deviceToken") String deviceToken, + @RequestParam(value = "chunkSize", required = false, defaultValue = "0") int chunkSize, + @RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) { + DeferredResult responseWriter = new DeferredResult<>(); + transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), + new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { + TransportProtos.GetFirmwareRequestMsg requestMsg = TransportProtos.GetFirmwareRequestMsg.newBuilder() + .setTenantIdMSB(sessionInfo.getTenantIdMSB()) + .setTenantIdLSB(sessionInfo.getTenantIdLSB()) + .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) + .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()).build(); + transportContext.getTransportService().process(sessionInfo, requestMsg, new GetFirmwareCallback(responseWriter, chunkSize, chunk)); + })); + return responseWriter; + } + @RequestMapping(value = "/provision", method = RequestMethod.POST) public DeferredResult provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult<>(); @@ -258,6 +276,41 @@ public class DeviceApiController { } } + private class GetFirmwareCallback implements TransportServiceCallback { + private final DeferredResult responseWriter; + private final int chuckSize; + private final int chuck; + + GetFirmwareCallback(DeferredResult responseWriter, int chuckSize, int chuck) { + this.responseWriter = responseWriter; + this.chuckSize = chuckSize; + this.chuck = chuck; + } + + @Override + public void onSuccess(TransportProtos.GetFirmwareResponseMsg firmwareResponseMsg) { + if (!TransportProtos.ResponseStatus.SUCCESS.equals(firmwareResponseMsg.getResponseStatus())) { + responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } else { + String firmwareId = new UUID(firmwareResponseMsg.getFirmwareIdMSB(), firmwareResponseMsg.getFirmwareIdLSB()).toString(); + ByteArrayResource resource = new ByteArrayResource(transportContext.getFirmwareCacheReader().get(firmwareId, chuckSize, chuck)); + ResponseEntity response = ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + firmwareResponseMsg.getFileName()) + .header("x-filename", firmwareResponseMsg.getFileName()) + .contentLength(resource.contentLength()) + .contentType(parseMediaType(firmwareResponseMsg.getContentType())) + .body(resource); + responseWriter.setResult(response); + } + } + + @Override + public void onError(Throwable e) { + log.warn("Failed to process request", e); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + } + private static class SessionCloseOnErrorCallback implements TransportServiceCallback { private final TransportService transportService; private final SessionInfoProto sessionInfo; @@ -338,4 +391,12 @@ public class DeviceApiController { .build(), TransportServiceCallback.EMPTY); } + private static MediaType parseMediaType(String contentType) { + try { + return MediaType.parseMediaType(contentType); + } catch (Exception e) { + return MediaType.APPLICATION_OCTET_STREAM; + } + } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java index 7d05630ba4..fa0cf67694 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -29,7 +29,6 @@ import redis.clients.jedis.ScanResult; import java.util.Collection; import java.util.LinkedList; -@Service public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { private static final String SEC_EP = "SEC#EP#"; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 6056aa293d..73ca2cdf9d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -40,12 +40,14 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.transport.SessionMsgListener; @@ -69,10 +71,10 @@ import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; import org.thingsboard.server.common.transport.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import java.io.IOException; import java.net.InetSocketAddress; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -80,7 +82,10 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static com.amazonaws.util.StringUtils.UTF8; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; @@ -99,6 +104,10 @@ import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE; @Slf4j public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener>, SessionMsgListener { + private static final Pattern FW_PATTERN = Pattern.compile("v2/fw/request/(?\\d+)/chunk/(?\\d+)"); + + private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"; + private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; private final UUID sessionId; @@ -112,6 +121,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private volatile InetSocketAddress address; private volatile GatewaySessionHandler gatewaySessionHandler; + private final ConcurrentHashMap fwSessions; + private final ConcurrentHashMap fwChunkSizes; + MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) { this.sessionId = UUID.randomUUID(); this.context = context; @@ -120,6 +132,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.sslHandler = sslHandler; this.mqttQoSMap = new ConcurrentHashMap<>(); this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context); + this.fwSessions = new ConcurrentHashMap<>(); + this.fwChunkSizes = new ConcurrentHashMap<>(); } @Override @@ -280,6 +294,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { + Matcher fwMatcher; MqttTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); @@ -299,6 +314,38 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); + } else if ((fwMatcher = FW_PATTERN.matcher(topicName)).find()) { + String payload = mqttMsg.content().toString(UTF8); + int chunkSize = StringUtils.isNotEmpty(payload) ? Integer.parseInt(payload) : 0; + String requestId = fwMatcher.group("requestId"); + int chunk = Integer.parseInt(fwMatcher.group("chunk")); + + if (chunkSize > 0) { + this.fwChunkSizes.put(requestId, chunkSize); + } else { + chunkSize = fwChunkSizes.getOrDefault(requestId, 0); + } + + if (chunkSize > context.getMaxPayloadSize()) { + sendFirmwareError(ctx, PAYLOAD_TOO_LARGE); + return; + } + + String firmwareId = fwSessions.get(requestId); + + if (firmwareId != null) { + sendFirmware(ctx, mqttMsg.variableHeader().packetId(), firmwareId, requestId, chunkSize, chunk); + } else { + TransportProtos.SessionInfoProto sessionInfo = deviceSessionCtx.getSessionInfo(); + TransportProtos.GetFirmwareRequestMsg getFirmwareRequestMsg = TransportProtos.GetFirmwareRequestMsg.newBuilder() + .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) + .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) + .setTenantIdMSB(sessionInfo.getTenantIdMSB()) + .setTenantIdLSB(sessionInfo.getTenantIdLSB()) + .build(); + transportService.process(deviceSessionCtx.getSessionInfo(), getFirmwareRequestMsg, + new FirmwareCallback(ctx, mqttMsg.variableHeader().packetId(), getFirmwareRequestMsg, requestId, chunkSize, chunk)); + } } else { transportService.reportActivity(deviceSessionCtx.getSessionInfo()); ack(ctx, msgId); @@ -366,6 +413,65 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private class FirmwareCallback implements TransportServiceCallback { + private final ChannelHandlerContext ctx; + private final int msgId; + private final TransportProtos.GetFirmwareRequestMsg msg; + private final String requestId; + private final int chunkSize; + private final int chunk; + + FirmwareCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.GetFirmwareRequestMsg msg, String requestId, int chunkSize, int chunk) { + this.ctx = ctx; + this.msgId = msgId; + this.msg = msg; + this.requestId = requestId; + this.chunkSize = chunkSize; + this.chunk = chunk; + } + + @Override + public void onSuccess(TransportProtos.GetFirmwareResponseMsg response) { + if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { + FirmwareId firmwareId = new FirmwareId(new UUID(response.getFirmwareIdMSB(), response.getFirmwareIdLSB())); + fwSessions.put(requestId, firmwareId.toString()); + sendFirmware(ctx, msgId, firmwareId.toString(), requestId, chunkSize, chunk); + } else { + sendFirmwareError(ctx, response.getResponseStatus().toString()); + } + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to get firmware: {}", sessionId, msg, e); + processDisconnect(ctx); + } + } + + private void sendFirmware(ChannelHandlerContext ctx, int msgId, String firmwareId, String requestId, int chunkSize, int chunk) { + log.trace("[{}] Send firmware [{}] to device!", sessionId, firmwareId); + ack(ctx, msgId); + try { + byte[] firmwareChunk = context.getFirmwareCacheReader().get(firmwareId, chunkSize, chunk); + deviceSessionCtx.getPayloadAdaptor() + .convertToPublish(deviceSessionCtx, firmwareChunk, requestId, chunk) + .ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + if (firmwareChunk != null && chunkSize != firmwareChunk.length) { + scheduler.schedule(() -> processDisconnect(ctx), 60, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.trace("[{}] Failed to send firmware response!", sessionId, e); + } + } + + private void sendFirmwareError(ChannelHandlerContext ctx, String error) { + log.warn("[{}] {}", sessionId, error); + deviceSessionCtx.getChannel().writeAndFlush(deviceSessionCtx + .getPayloadAdaptor() + .createMqttPublishMsg(deviceSessionCtx, MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC, error.getBytes())); + processDisconnect(ctx); + } + private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { if (!checkConnected(ctx, mqttMsg)) { return; @@ -396,6 +502,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement case MqttTopics.GATEWAY_RPC_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: registerSubQoS(topic, grantedQoSList, reqQoS); break; default: diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 47a3a9fe87..8d0bf312a0 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -21,8 +21,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageType; @@ -31,10 +29,10 @@ import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; import java.nio.charset.Charset; @@ -55,7 +53,6 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { protected static final Charset UTF8 = StandardCharsets.UTF_8; private static final Gson GSON = new Gson(); - private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); @Override public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { @@ -153,6 +150,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse))); } + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk) { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_FIRMWARE_RESPONSE_TOPIC_PREFIX + requestId + "/chunk/" + chunk, firmwareChunk)); + } + public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { String payload = validatePayload(sessionId, payloadData, false); try { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index e8a455a923..2bd35df8d9 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -15,8 +15,14 @@ */ package org.thingsboard.server.transport.mqtt.adaptors; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; @@ -39,6 +45,8 @@ import java.util.Optional; */ public interface MqttTransportAdaptor { + ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); + PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; @@ -69,4 +77,14 @@ public interface MqttTransportAdaptor { Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException; + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk) throws AdaptorException; + + default MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadInBytes) { + MqttFixedHeader mqttFixedHeader = + new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); + MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); + ByteBuf payload = ALLOCATOR.buffer(); + payload.writeBytes(payloadInBytes); + return new MqttPublishMessage(mqttFixedHeader, header, payload); + } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index c948e755c2..8dfec2003c 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -21,13 +21,8 @@ import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; -import io.netty.handler.codec.mqtt.MqttFixedHeader; import io.netty.handler.codec.mqtt.MqttMessage; -import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttPublishMessage; -import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -46,8 +41,6 @@ import java.util.Optional; @Slf4j public class ProtoMqttAdaptor implements MqttTransportAdaptor { - private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); - @Override public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; @@ -143,7 +136,6 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { } } - @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest))); @@ -164,6 +156,11 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray())); } + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk) throws AdaptorException { + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_FIRMWARE_RESPONSE_TOPIC_PREFIX + requestId + "/" + chunk, firmwareChunk)); + } + @Override public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { @@ -202,15 +199,6 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { return bytes; } - private MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadBytes) { - MqttFixedHeader mqttFixedHeader = - new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); - MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); - ByteBuf payload = ALLOCATOR.buffer(); - payload.writeBytes(payloadBytes); - return new MqttPublishMessage(mqttFixedHeader, header, payload); - } - private int getRequestId(String topicName, String topic) { return Integer.parseInt(topicName.substring(topic.length())); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index 75f20cc825..a1d09b78a7 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -20,10 +20,9 @@ import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.firmware.FirmwareCacheReader; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.scheduler.SchedulerComponent; -import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -51,6 +50,11 @@ public abstract class TransportContext { @Getter private ExecutorService executor; + + @Getter + @Autowired + private FirmwareCacheReader firmwareCacheReader; + @PostConstruct public void init() { executor = Executors.newWorkStealingPool(50); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 04b08272b1..c851be2586 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -24,6 +24,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetFirmwareRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetFirmwareResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetResourceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetResourceResponseMsg; @@ -98,6 +100,8 @@ public interface TransportService { void process(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback callback); + void process(SessionInfoProto sessionInfoProto, GetFirmwareRequestMsg msg, TransportServiceCallback callback); + SessionMetaData registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener); SessionMetaData registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index c2add634d9..6155dcac7a 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -44,7 +44,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus; +import org.thingsboard.server.gen.transport.TransportProtos.ResponseStatus; import org.thingsboard.server.gen.transport.TransportProtos.TsKvListProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; @@ -423,12 +423,12 @@ public class JsonConverter { private static JsonObject toJson(ProvisionDeviceResponseMsg payload, boolean toGateway, int requestId) { JsonObject result = new JsonObject(); - if (payload.getStatus() == TransportProtos.ProvisionResponseStatus.NOT_FOUND) { + if (payload.getStatus() == ResponseStatus.NOT_FOUND) { result.addProperty("errorMsg", "Provision data was not found!"); - result.addProperty("status", ProvisionResponseStatus.NOT_FOUND.name()); - } else if (payload.getStatus() == TransportProtos.ProvisionResponseStatus.FAILURE) { + result.addProperty("status", ResponseStatus.NOT_FOUND.name()); + } else if (payload.getStatus() == TransportProtos.ResponseStatus.FAILURE) { result.addProperty("errorMsg", "Failed to provision device!"); - result.addProperty("status", ProvisionResponseStatus.FAILURE.name()); + result.addProperty("status", ResponseStatus.FAILURE.name()); } else { if (toGateway) { result.addProperty("id", requestId); @@ -445,7 +445,7 @@ public class JsonConverter { break; } result.addProperty("credentialsType", payload.getCredentialsType().name()); - result.addProperty("status", ProvisionResponseStatus.SUCCESS.name()); + result.addProperty("status", ResponseStatus.SUCCESS.name()); } return result; } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 740a2a622f..fa28627882 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -377,7 +377,6 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } - @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback callback) { if (log.isTraceEnabled()) { @@ -529,6 +528,19 @@ public class DefaultTransportService implements TransportService { } } + @Override + public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetFirmwareRequestMsg msg, TransportServiceCallback callback) { + if (checkLimits(sessionInfo, msg, callback)) { + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setFirmwareRequestMsg(msg).build()); + + AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), response -> { + TransportProtos.GetFirmwareResponseMsg firmwareResponseMsg = response.getValue().getFirmwareResponseMsg(); + callback.onSuccess(firmwareResponseMsg); + }, callback::onError, transportCallbackExecutor); + } + } + @Override public SessionMetaData reportActivity(TransportProtos.SessionInfoProto sessionInfo) { return reportActivityInternal(sessionInfo); @@ -608,11 +620,11 @@ public class DefaultTransportService implements TransportService { sessions.remove(toSessionId(sessionInfo)); } - private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback) { + private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback) { return checkLimits(sessionInfo, msg, callback, 0); } - private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback, int dataPoints) { + private boolean checkLimits(TransportProtos.SessionInfoProto sessionInfo, Object msg, TransportServiceCallback callback, int dataPoints) { if (log.isTraceEnabled()) { log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index bff523d956..9f85dd1cb5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Firmware; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.CoapDeviceTypeConfiguration; @@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -107,6 +109,9 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D @Autowired private CacheManager cacheManager; + @Autowired + private FirmwareService firmwareService; + private final Lock findOrCreateLock = new ReentrantLock(); @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#deviceProfileId.id}") @@ -389,6 +394,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } } + if (deviceProfile.getFirmwareId() != null) { + Firmware firmware = firmwareService.findFirmwareById(tenantId, deviceProfile.getFirmwareId()); + if (firmware == null) { + throw new DataValidationException("Can't assign non-existent firmware!"); + } + if (firmware.getData() == null) { + throw new DataValidationException("Can't assign firmware with empty data!"); + } + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 7ce2e650d2..d1d76b1e95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Firmware; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; @@ -68,6 +69,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.firmware.FirmwareService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -128,6 +130,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Lazy private TbTenantProfileCache tenantProfileCache; + @Autowired + private FirmwareService firmwareService; + @Override public DeviceInfo findDeviceInfoById(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceInfoById [{}]", deviceId); @@ -598,6 +603,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe throw new DataValidationException("Can't assign device to customer from different tenant!"); } } + + if (device.getFirmwareId() != null) { + Firmware firmware = firmwareService.findFirmwareById(tenantId, device.getFirmwareId()); + if (firmware == null) { + throw new DataValidationException("Can't assign non-existent firmware!"); + } + if (firmware.getData() == null) { + throw new DataValidationException("Can't assign firmware with empty data!"); + } + } } }; diff --git a/dao/src/main/java/org/thingsboard/server/dao/firmware/BaseFirmwareService.java b/dao/src/main/java/org/thingsboard/server/dao/firmware/BaseFirmwareService.java new file mode 100644 index 0000000000..df3291b486 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/firmware/BaseFirmwareService.java @@ -0,0 +1,317 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.firmware; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TenantDao; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Optional; + +import static org.thingsboard.server.common.data.CacheConstants.FIRMWARE_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + +@Service +@Slf4j +public class BaseFirmwareService implements FirmwareService { + public static final String INCORRECT_FIRMWARE_ID = "Incorrect firmwareId "; + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + private final TenantDao tenantDao; + private final FirmwareDao firmwareDao; + private final FirmwareInfoDao firmwareInfoDao; + private final CacheManager cacheManager; + + public BaseFirmwareService(TenantDao tenantDao, FirmwareDao firmwareDao, FirmwareInfoDao firmwareInfoDao, CacheManager cacheManager) { + this.tenantDao = tenantDao; + this.firmwareDao = firmwareDao; + this.firmwareInfoDao = firmwareInfoDao; + this.cacheManager = cacheManager; + } + + @Override + public FirmwareInfo saveFirmwareInfo(FirmwareInfo firmwareInfo) { + log.trace("Executing saveFirmwareInfo [{}]", firmwareInfo); + firmwareInfoValidator.validate(firmwareInfo, FirmwareInfo::getTenantId); + try { + FirmwareId firmwareId = firmwareInfo.getId(); + if (firmwareId != null) { + cacheManager.getCache(FIRMWARE_CACHE).evict(firmwareId.toString()); + } + return firmwareInfoDao.save(firmwareInfo.getTenantId(), firmwareInfo); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("firmware_tenant_title_version_unq_key")) { + throw new DataValidationException("Firmware with such title and version already exists!"); + } else { + throw t; + } + } + } + + @Override + public Firmware saveFirmware(Firmware firmware) { + log.trace("Executing saveFirmware [{}]", firmware); + firmwareValidator.validate(firmware, FirmwareInfo::getTenantId); + try { + FirmwareId firmwareId = firmware.getId(); + if (firmwareId != null) { + cacheManager.getCache(FIRMWARE_CACHE).evict(firmwareId.toString()); + } + return firmwareDao.save(firmware.getTenantId(), firmware); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("firmware_tenant_title_version_unq_key")) { + throw new DataValidationException("Firmware with such title and version already exists!"); + } else { + throw t; + } + } + } + + @Override + public Firmware findFirmwareById(TenantId tenantId, FirmwareId firmwareId) { + log.trace("Executing findFirmwareById [{}]", firmwareId); + validateId(firmwareId, INCORRECT_FIRMWARE_ID + firmwareId); + return firmwareDao.findById(tenantId, firmwareId.getId()); + } + + @Override + public FirmwareInfo findFirmwareInfoById(TenantId tenantId, FirmwareId firmwareId) { + log.trace("Executing findFirmwareInfoById [{}]", firmwareId); + validateId(firmwareId, INCORRECT_FIRMWARE_ID + firmwareId); + return firmwareInfoDao.findById(tenantId, firmwareId.getId()); + } + + @Override + public PageData findTenantFirmwaresByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findTenantFirmwaresByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return firmwareInfoDao.findFirmwareInfoByTenantId(tenantId, pageLink); + } + + @Override + public PageData findTenantFirmwaresByTenantIdAndHasData(TenantId tenantId, boolean hasData, PageLink pageLink) { + log.trace("Executing findTenantFirmwaresByTenantIdAndHasData, tenantId [{}], hasData [{}] pageLink [{}]", tenantId, hasData, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return firmwareInfoDao.findFirmwareInfoByTenantIdAndHasData(tenantId, hasData, pageLink); + } + + @Override + public void deleteFirmware(TenantId tenantId, FirmwareId firmwareId) { + log.trace("Executing deleteFirmware [{}]", firmwareId); + validateId(firmwareId, INCORRECT_FIRMWARE_ID + firmwareId); + try { + Cache cache = cacheManager.getCache(FIRMWARE_CACHE); + cache.evict(Collections.singletonList(firmwareId)); + firmwareDao.removeById(tenantId, firmwareId.getId()); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_firmware_device")) { + throw new DataValidationException("The firmware referenced by the devices cannot be deleted!"); + } else if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_firmware_device_profile")) { + throw new DataValidationException("The firmware referenced by the device profile cannot be deleted!"); + } else { + throw t; + } + } + } + + @Override + public void deleteFirmwaresByTenantId(TenantId tenantId) { + log.trace("Executing deleteFirmwaresByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantFirmwareRemover.removeEntities(tenantId, tenantId); + } + + private DataValidator firmwareInfoValidator = new DataValidator<>() { + + @Override + protected void validateDataImpl(TenantId tenantId, FirmwareInfo firmware) { + if (firmware.getTenantId() == null) { + throw new DataValidationException("Firmware should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(firmware.getTenantId(), firmware.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Firmware is referencing to non-existent tenant!"); + } + } + + if (StringUtils.isEmpty(firmware.getTitle())) { + throw new DataValidationException("Firmware title should be specified!"); + } + + if (StringUtils.isEmpty(firmware.getVersion())) { + throw new DataValidationException("Firmware version should be specified!"); + } + } + + @Override + protected void validateUpdate(TenantId tenantId, FirmwareInfo firmware) { + FirmwareInfo firmwareOld = firmwareInfoDao.findById(tenantId, firmware.getUuidId()); + + if (!firmwareOld.getTitle().equals(firmware.getTitle())) { + throw new DataValidationException("Updating firmware title is prohibited!"); + } + + if (!firmwareOld.getVersion().equals(firmware.getVersion())) { + throw new DataValidationException("Updating firmware version is prohibited!"); + } + } + }; + + private DataValidator firmwareValidator = new DataValidator<>() { + + @Override + protected void validateDataImpl(TenantId tenantId, Firmware firmware) { + if (firmware.getTenantId() == null) { + throw new DataValidationException("Firmware should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(firmware.getTenantId(), firmware.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Firmware is referencing to non-existent tenant!"); + } + } + + if (StringUtils.isEmpty(firmware.getTitle())) { + throw new DataValidationException("Firmware title should be specified!"); + } + + if (StringUtils.isEmpty(firmware.getVersion())) { + throw new DataValidationException("Firmware version should be specified!"); + } + + if (StringUtils.isEmpty(firmware.getFileName())) { + throw new DataValidationException("Firmware file name should be specified!"); + } + + if (StringUtils.isEmpty(firmware.getContentType())) { + throw new DataValidationException("Firmware content type should be specified!"); + } + + ByteBuffer data = firmware.getData(); + if (data == null || !data.hasArray() || data.array().length == 0) { + throw new DataValidationException("Firmware data should be specified!"); + } + + if (StringUtils.isEmpty(firmware.getChecksumAlgorithm())) { + throw new DataValidationException("Firmware checksum algorithm should be specified!"); + } + if (StringUtils.isEmpty(firmware.getChecksum())) { + throw new DataValidationException("Firmware checksum should be specified!"); + } + + HashFunction hashFunction; + switch (firmware.getChecksumAlgorithm()) { + case "sha256": + hashFunction = Hashing.sha256(); + break; + case "md5": + hashFunction = Hashing.md5(); + break; + case "crc32": + hashFunction = Hashing.crc32(); + break; + default: + throw new DataValidationException("Unknown checksum algorithm!"); + } + + String currentChecksum = hashFunction.hashBytes(data.array()).toString(); + + if (!currentChecksum.equals(firmware.getChecksum())) { + throw new DataValidationException("Wrong firmware file!"); + } + } + + @Override + protected void validateUpdate(TenantId tenantId, Firmware firmware) { + Firmware firmwareOld = firmwareDao.findById(tenantId, firmware.getUuidId()); + + if (!firmwareOld.getTitle().equals(firmware.getTitle())) { + throw new DataValidationException("Updating firmware title is prohibited!"); + } + + if (!firmwareOld.getVersion().equals(firmware.getVersion())) { + throw new DataValidationException("Updating firmware version is prohibited!"); + } + + if (firmwareOld.getFileName() != null && !firmwareOld.getFileName().equals(firmware.getFileName())) { + throw new DataValidationException("Updating firmware file name is prohibited!"); + } + + if (firmwareOld.getContentType() != null && !firmwareOld.getContentType().equals(firmware.getContentType())) { + throw new DataValidationException("Updating firmware content type is prohibited!"); + } + + if (firmwareOld.getChecksumAlgorithm() != null && !firmwareOld.getChecksumAlgorithm().equals(firmware.getChecksumAlgorithm())) { + throw new DataValidationException("Updating firmware content type is prohibited!"); + } + + if (firmwareOld.getChecksum() != null && !firmwareOld.getChecksum().equals(firmware.getChecksum())) { + throw new DataValidationException("Updating firmware content type is prohibited!"); + } + + if (firmwareOld.getData() != null && !firmwareOld.getData().equals(firmware.getData())) { + throw new DataValidationException("Updating firmware data is prohibited!"); + } + } + }; + + private PaginatedRemover tenantFirmwareRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return firmwareInfoDao.findFirmwareInfoByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, FirmwareInfo entity) { + deleteFirmware(tenantId, entity.getId()); + } + }; + + protected Optional extractConstraintViolationException(Exception t) { + if (t instanceof ConstraintViolationException) { + return Optional.of((ConstraintViolationException) t); + } else if (t.getCause() instanceof ConstraintViolationException) { + return Optional.of((ConstraintViolationException) (t.getCause())); + } else { + return Optional.empty(); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareDao.java b/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareDao.java new file mode 100644 index 0000000000..0cacb47ea0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareDao.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.firmware; + +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.dao.Dao; + +public interface FirmwareDao extends Dao { + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareInfoDao.java new file mode 100644 index 0000000000..a2af06ad9a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/firmware/FirmwareInfoDao.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.firmware; + +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.UUID; + +public interface FirmwareInfoDao extends Dao { + + PageData findFirmwareInfoByTenantId(TenantId tenantId, PageLink pageLink); + + PageData findFirmwareInfoByTenantIdAndHasData(TenantId tenantId, boolean hasData, PageLink pageLink); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3afae3c081..0fb748c83a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -153,6 +153,7 @@ public class ModelConstants { public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String DEVICE_DEVICE_PROFILE_ID_PROPERTY = "device_profile_id"; public static final String DEVICE_DEVICE_DATA_PROPERTY = "device_data"; + public static final String DEVICE_FIRMWARE_ID_PROPERTY = "firmware_id"; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; @@ -176,6 +177,7 @@ public class ModelConstants { public static final String DEVICE_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY = "default_rule_chain_id"; public static final String DEVICE_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY = "default_queue_name"; public static final String DEVICE_PROFILE_PROVISION_DEVICE_KEY = "provision_device_key"; + public static final String DEVICE_PROFILE_FIRMWARE_ID_PROPERTY = "firmware_id"; /** * Cassandra entityView constants. @@ -467,6 +469,22 @@ public class ModelConstants { public static final String RESOURCE_FILE_NAME_COLUMN = "file_name"; public static final String RESOURCE_DATA_COLUMN = "data"; + /** + * Firmware constants. + */ + public static final String FIRMWARE_TABLE_NAME = "firmware"; + public static final String FIRMWARE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; + public static final String FIRMWARE_TITLE_COLUMN = TITLE_PROPERTY; + public static final String FIRMWARE_VERSION_COLUMN = "version"; + public static final String FIRMWARE_FILE_NAME_COLUMN = "file_name"; + public static final String FIRMWARE_CONTENT_TYPE_COLUMN = "content_type"; + public static final String FIRMWARE_CHECKSUM_ALGORITHM_COLUMN = "checksum_algorithm"; + public static final String FIRMWARE_CHECKSUM_COLUMN = "checksum"; + public static final String FIRMWARE_DATA_COLUMN = "data"; + public static final String FIRMWARE_ADDITIONAL_INFO_COLUMN = ADDITIONAL_INFO_PROPERTY; + public static final String FIRMWARE_HAS_DATA_PROPERTY = "has_data"; + + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java index 5a4381fa24..8dee576611 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -73,6 +74,9 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti @Column(name = ModelConstants.DEVICE_DEVICE_PROFILE_ID_PROPERTY, columnDefinition = "uuid") private UUID deviceProfileId; + @Column(name = ModelConstants.DEVICE_FIRMWARE_ID_PROPERTY, columnDefinition = "uuid") + private UUID firmwareId; + @Type(type = "jsonb") @Column(name = ModelConstants.DEVICE_DEVICE_DATA_PROPERTY, columnDefinition = "jsonb") private JsonNode deviceData; @@ -95,6 +99,9 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti if (device.getDeviceProfileId() != null) { this.deviceProfileId = device.getDeviceProfileId().getId(); } + if (device.getFirmwareId() != null) { + this.firmwareId = device.getFirmwareId().getId(); + } this.deviceData = JacksonUtil.convertValue(device.getDeviceData(), ObjectNode.class); this.name = device.getName(); this.type = device.getType(); @@ -114,6 +121,7 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti this.label = deviceEntity.getLabel(); this.searchText = deviceEntity.getSearchText(); this.additionalInfo = deviceEntity.getAdditionalInfo(); + this.firmwareId = deviceEntity.getFirmwareId(); } @Override @@ -138,6 +146,9 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti if (deviceProfileId != null) { device.setDeviceProfileId(new DeviceProfileId(deviceProfileId)); } + if (firmwareId != null) { + device.setFirmwareId(new FirmwareId(firmwareId)); + } device.setDeviceData(JacksonUtil.convertValue(deviceData, DeviceData.class)); device.setName(name); device.setType(type); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index 0ddf78c8ce..e437c87fdd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.FirmwareId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -89,6 +90,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl @Column(name=ModelConstants.DEVICE_PROFILE_PROVISION_DEVICE_KEY) private String provisionDeviceKey; + @Column(name=ModelConstants.DEVICE_PROFILE_FIRMWARE_ID_PROPERTY) + private UUID firmwareId; + public DeviceProfileEntity() { super(); } @@ -113,6 +117,9 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl } this.defaultQueueName = deviceProfile.getDefaultQueueName(); this.provisionDeviceKey = deviceProfile.getProvisionDeviceKey(); + if (deviceProfile.getFirmwareId() != null) { + this.firmwareId = deviceProfile.getFirmwareId().getId(); + } } @Override @@ -148,6 +155,11 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl } deviceProfile.setDefaultQueueName(defaultQueueName); deviceProfile.setProvisionDeviceKey(provisionDeviceKey); + + if (firmwareId != null) { + deviceProfile.setFirmwareId(new FirmwareId(firmwareId)); + } + return deviceProfile; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareEntity.java new file mode 100644 index 0000000000..2da0b110cc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareEntity.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.nio.ByteBuffer; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_CHECKSUM_ALGORITHM_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_CHECKSUM_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_CONTENT_TYPE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_DATA_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_FILE_NAME_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TITLE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_VERSION_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = FIRMWARE_TABLE_NAME) +public class FirmwareEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = FIRMWARE_TENANT_ID_COLUMN) + private UUID tenantId; + + @Column(name = FIRMWARE_TITLE_COLUMN) + private String title; + + @Column(name = FIRMWARE_VERSION_COLUMN) + private String version; + + @Column(name = FIRMWARE_FILE_NAME_COLUMN) + private String fileName; + + @Column(name = FIRMWARE_CONTENT_TYPE_COLUMN) + private String contentType; + + @Column(name = FIRMWARE_CHECKSUM_ALGORITHM_COLUMN) + private String checksumAlgorithm; + + @Column(name = FIRMWARE_CHECKSUM_COLUMN) + private String checksum; + + @Column(name = FIRMWARE_DATA_COLUMN, columnDefinition = "BINARY") + private byte[] data; + + @Type(type = "json") + @Column(name = ModelConstants.FIRMWARE_ADDITIONAL_INFO_COLUMN) + private JsonNode additionalInfo; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + + public FirmwareEntity() { + super(); + } + + public FirmwareEntity(Firmware firmware) { + this.createdTime = firmware.getCreatedTime(); + this.setUuid(firmware.getUuidId()); + this.tenantId = firmware.getTenantId().getId(); + this.title = firmware.getTitle(); + this.version = firmware.getVersion(); + this.fileName = firmware.getFileName(); + this.contentType = firmware.getContentType(); + this.checksumAlgorithm = firmware.getChecksumAlgorithm(); + this.checksum = firmware.getChecksum(); + this.data = firmware.getData().array(); + this.additionalInfo = firmware.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return title; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + @Override + public Firmware toData() { + Firmware firmware = new Firmware(new FirmwareId(id)); + firmware.setCreatedTime(createdTime); + firmware.setTenantId(new TenantId(tenantId)); + firmware.setTitle(title); + firmware.setVersion(version); + firmware.setFileName(fileName); + firmware.setContentType(contentType); + firmware.setChecksumAlgorithm(checksumAlgorithm); + firmware.setChecksum(checksum); + if (data != null) { + firmware.setData(ByteBuffer.wrap(data)); + firmware.setHasData(true); + } + firmware.setAdditionalInfo(additionalInfo); + return firmware; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareInfoEntity.java new file mode 100644 index 0000000000..ea0bd7ffe0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/FirmwareInfoEntity.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.id.FirmwareId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.Transient; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_HAS_DATA_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_TITLE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.FIRMWARE_VERSION_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = FIRMWARE_TABLE_NAME) +public class FirmwareInfoEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = FIRMWARE_TENANT_ID_COLUMN) + private UUID tenantId; + + @Column(name = FIRMWARE_TITLE_COLUMN) + private String title; + + @Column(name = FIRMWARE_VERSION_COLUMN) + private String version; + + @Type(type = "json") + @Column(name = ModelConstants.FIRMWARE_ADDITIONAL_INFO_COLUMN) + private JsonNode additionalInfo; + + @Column(name = SEARCH_TEXT_PROPERTY) + private String searchText; + +// @Column(name = FIRMWARE_HAS_DATA_PROPERTY, insertable = false, updatable = false) + @Transient + private boolean hasData; + + public FirmwareInfoEntity() { + super(); + } + + public FirmwareInfoEntity(FirmwareInfo firmware) { + this.createdTime = firmware.getCreatedTime(); + this.setUuid(firmware.getUuidId()); + this.tenantId = firmware.getTenantId().getId(); + this.title = firmware.getTitle(); + this.version = firmware.getVersion(); + this.additionalInfo = firmware.getAdditionalInfo(); + } + + public FirmwareInfoEntity(UUID id, long createdTime, UUID tenantId, String title, String version, Object additionalInfo, boolean hasData) { + this.id = id; + this.createdTime = createdTime; + this.tenantId = tenantId; + this.title = title; + this.version = version; + this.hasData = hasData; + this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class); + } + + @Override + public String getSearchTextSource() { + return title; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + @Override + public FirmwareInfo toData() { + FirmwareInfo firmware = new FirmwareInfo(new FirmwareId(id)); + firmware.setCreatedTime(createdTime); + firmware.setTenantId(new TenantId(tenantId)); + firmware.setTitle(title); + firmware.setVersion(version); + firmware.setAdditionalInfo(additionalInfo); + firmware.setHasData(hasData); + return firmware; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareInfoRepository.java new file mode 100644 index 0000000000..8dec49dca9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareInfoRepository.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.firmware; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.FirmwareInfoEntity; + +import java.util.UUID; + +public interface FirmwareInfoRepository extends CrudRepository { + @Query("SELECT new FirmwareInfoEntity(f.id, f.createdTime, f.tenantId, f.title, f.version, f.additionalInfo, f.data IS NOT NULL) FROM FirmwareEntity f WHERE " + + "f.tenantId = :tenantId " + + "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findAllByTenantId(@Param("tenantId") UUID tenantId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new FirmwareInfoEntity(f.id, f.createdTime, f.tenantId, f.title, f.version, f.additionalInfo, f.data IS NOT NULL) FROM FirmwareEntity f WHERE " + + "f.tenantId = :tenantId " + + "AND ((f.data IS NOT NULL AND :hasData = true) OR (f.data IS NULL AND :hasData = false ))" + + "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + Page findAllByTenantIdAndHasData(@Param("tenantId") UUID tenantId, + @Param("hasData") boolean hasData, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new FirmwareInfoEntity(f.id, f.createdTime, f.tenantId, f.title, f.version, f.additionalInfo, f.data IS NOT NULL) FROM FirmwareEntity f WHERE f.id = :id") + FirmwareInfoEntity findFirmwareInfoById(@Param("id") UUID id); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareRepository.java new file mode 100644 index 0000000000..a969507798 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/FirmwareRepository.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.firmware; + +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.FirmwareEntity; + +import java.util.UUID; + +public interface FirmwareRepository extends CrudRepository { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareDao.java new file mode 100644 index 0000000000..44b956c298 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareDao.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.firmware; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.dao.firmware.FirmwareDao; +import org.thingsboard.server.dao.model.sql.FirmwareEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.UUID; + +@Slf4j +@Component +public class JpaFirmwareDao extends JpaAbstractSearchTextDao implements FirmwareDao { + + @Autowired + private FirmwareRepository firmwareRepository; + + @Override + protected Class getEntityClass() { + return FirmwareEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return firmwareRepository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareInfoDao.java new file mode 100644 index 0000000000..3b80c1c7e5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/firmware/JpaFirmwareInfoDao.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.firmware; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.firmware.FirmwareInfoDao; +import org.thingsboard.server.dao.model.sql.FirmwareInfoEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class JpaFirmwareInfoDao extends JpaAbstractSearchTextDao implements FirmwareInfoDao { + + @Autowired + private FirmwareInfoRepository firmwareInfoRepository; + + @Override + protected Class getEntityClass() { + return FirmwareInfoEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return firmwareInfoRepository; + } + + @Override + public FirmwareInfo findById(TenantId tenantId, UUID id) { + return DaoUtil.getData(firmwareInfoRepository.findFirmwareInfoById(id)); + } + + @Override + public FirmwareInfo save(TenantId tenantId, FirmwareInfo firmwareInfo) { + FirmwareInfo savedFirmware = super.save(tenantId, firmwareInfo); + if (firmwareInfo.getId() == null) { + return savedFirmware; + } else { + return findById(tenantId, savedFirmware.getId().getId()); + } + } + + @Override + public PageData findFirmwareInfoByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(firmwareInfoRepository + .findAllByTenantId( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findFirmwareInfoByTenantIdAndHasData(TenantId tenantId, boolean hasData, PageLink pageLink) { + return DaoUtil.toPageData(firmwareInfoRepository + .findAllByTenantIdAndHasData( + tenantId.getId(), + hasData, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 488192eb13..c5587d36b2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -48,8 +48,6 @@ public interface TbResourceRepository extends CrudRepository implements Comparator { @Override public int compare(D o1, D o2) { @@ -192,7 +196,7 @@ public abstract class AbstractServiceTest { @Bean public AuditLogLevelFilter auditLogLevelFilter() { - Map mask = new HashMap<>(); + Map mask = new HashMap<>(); for (EntityType entityType : EntityType.values()) { mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java index 0fd8925fc9..e377baa2cb 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -27,19 +27,19 @@ import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; -import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Firmware; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.exception.DataValidationException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -82,18 +82,49 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); } + @Test + public void testSaveDeviceProfileWithFirmware() { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + Assert.assertNotNull(savedDeviceProfile); + Assert.assertNotNull(savedDeviceProfile.getId()); + Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); + Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName()); + Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription()); + Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData()); + Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault()); + Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId()); + + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle("my firmware"); + firmware.setVersion("v1.0"); + firmware.setFileName("test.txt"); + firmware.setContentType("text/plain"); + firmware.setChecksumAlgorithm("sha256"); + firmware.setChecksum("4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"); + firmware.setData(ByteBuffer.wrap(new byte[]{1})); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + deviceProfile.setFirmwareId(savedFirmware.getId()); + + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); + } + @Test public void testFindDeviceProfileById() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); Assert.assertNotNull(foundDeviceProfile); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); - } + } @Test public void testFindDeviceProfileInfoById() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfileInfo foundDeviceProfileInfo = deviceProfileService.findDeviceProfileInfoById(tenantId, savedDeviceProfile.getId()); Assert.assertNotNull(foundDeviceProfileInfo); @@ -124,7 +155,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { ListeningExecutorService testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100)); try { List> futures = new ArrayList<>(); - for (int i = 0; i < 50; i ++) { + for (int i = 0; i < 50; i++) { futures.add(testExecutor.submit(() -> deviceProfileService.findOrCreateDeviceProfile(tenantId, "Device Profile 1"))); futures.add(testExecutor.submit(() -> deviceProfileService.findOrCreateDeviceProfile(tenantId, "Device Profile 2"))); } @@ -138,8 +169,8 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { @Test public void testSetDefaultDeviceProfile() { - DeviceProfile deviceProfile1 = this.createDeviceProfile(tenantId,"Device Profile 1"); - DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile 2"); + DeviceProfile deviceProfile1 = this.createDeviceProfile(tenantId, "Device Profile 1"); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId, "Device Profile 2"); DeviceProfile savedDeviceProfile1 = deviceProfileService.saveDeviceProfile(deviceProfile1); DeviceProfile savedDeviceProfile2 = deviceProfileService.saveDeviceProfile(deviceProfile2); @@ -165,16 +196,16 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testSaveDeviceProfileWithSameName() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); deviceProfileService.saveDeviceProfile(deviceProfile); - DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId, "Device Profile"); deviceProfileService.saveDeviceProfile(deviceProfile2); } @Ignore @Test(expected = DataValidationException.class) public void testChangeDeviceProfileTypeWithExistingDevices() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); Device device = new Device(); device.setTenantId(tenantId); @@ -189,7 +220,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testChangeDeviceProfileTransportTypeWithExistingDevices() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); Device device = new Device(); device.setTenantId(tenantId); @@ -203,7 +234,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { @Test(expected = DataValidationException.class) public void testDeleteDeviceProfileWithExistingDevice() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); Device device = new Device(); device.setTenantId(tenantId); @@ -216,7 +247,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { @Test public void testDeleteDeviceProfile() { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"); + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); DeviceProfile foundDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, savedDeviceProfile.getId()); @@ -233,8 +264,8 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(1, pageData.getTotalElements()); deviceProfiles.addAll(pageData.getData()); - for (int i=0;i<28;i++) { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + for (int i = 0; i < 28; i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile" + i); deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); } @@ -275,8 +306,8 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(1, deviceProfilePageData.getTotalElements()); deviceProfiles.addAll(deviceProfilePageData.getData()); - for (int i=0;i<28;i++) { - DeviceProfile deviceProfile = this.createDeviceProfile(tenantId,"Device Profile"+i); + for (int i = 0; i < 28; i++) { + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile" + i); deviceProfiles.add(deviceProfileService.saveDeviceProfile(deviceProfile)); } @@ -297,7 +328,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { List deviceProfileInfos = deviceProfiles.stream() .map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(), - deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); + deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList()); Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos); @@ -312,4 +343,5 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(1, pageData.getTotalElements()); } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index b323d0088c..d9ca69632b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.exception.DataValidationException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -88,6 +89,49 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.deleteDevice(tenantId, savedDevice.getId()); } + + @Test + public void testSaveDeviceWithFirmware() { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("My device"); + device.setType("default"); + Device savedDevice = deviceService.saveDevice(device); + + Assert.assertNotNull(savedDevice); + Assert.assertNotNull(savedDevice.getId()); + Assert.assertTrue(savedDevice.getCreatedTime() > 0); + Assert.assertEquals(device.getTenantId(), savedDevice.getTenantId()); + Assert.assertNotNull(savedDevice.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId()); + Assert.assertEquals(device.getName(), savedDevice.getName()); + + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); + Assert.assertNotNull(deviceCredentials); + Assert.assertNotNull(deviceCredentials.getId()); + Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId()); + Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType()); + Assert.assertNotNull(deviceCredentials.getCredentialsId()); + Assert.assertEquals(20, deviceCredentials.getCredentialsId().length()); + + + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle("my firmware"); + firmware.setVersion("v1.0"); + firmware.setFileName("test.txt"); + firmware.setContentType("text/plain"); + firmware.setChecksumAlgorithm("sha256"); + firmware.setChecksum("4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"); + firmware.setData(ByteBuffer.wrap(new byte[]{1})); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + savedDevice.setFirmwareId(savedFirmware.getId()); + + deviceService.saveDevice(savedDevice); + Device foundDevice = deviceService.findDeviceById(tenantId, savedDevice.getId()); + Assert.assertEquals(foundDevice.getName(), savedDevice.getName()); + } @Test(expected = DataValidationException.class) public void testSaveDeviceWithEmptyName() { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseFirmwareServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseFirmwareServiceTest.java new file mode 100644 index 0000000000..54033e29a7 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseFirmwareServiceTest.java @@ -0,0 +1,481 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.Firmware; +import org.thingsboard.server.common.data.FirmwareInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class BaseFirmwareServiceTest extends AbstractServiceTest { + + public static final String TITLE = "My firmware"; + private static final String FILE_NAME = "filename.txt"; + private static final String VERSION = "v1.0"; + private static final String CONTENT_TYPE = "text/plain"; + private static final String CHECKSUM_ALGORITHM = "sha256"; + private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; + private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{1}); + + private IdComparator idComparator = new IdComparator<>(); + + private TenantId tenantId; + + @Before + public void before() { + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = tenantService.saveTenant(tenant); + Assert.assertNotNull(savedTenant); + tenantId = savedTenant.getId(); + } + + @After + public void after() { + tenantService.deleteTenant(tenantId); + } + + @Test + public void testSaveFirmware() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + Assert.assertNotNull(savedFirmware); + Assert.assertNotNull(savedFirmware.getId()); + Assert.assertTrue(savedFirmware.getCreatedTime() > 0); + Assert.assertEquals(firmware.getTenantId(), savedFirmware.getTenantId()); + Assert.assertEquals(firmware.getTitle(), savedFirmware.getTitle()); + Assert.assertEquals(firmware.getFileName(), savedFirmware.getFileName()); + Assert.assertEquals(firmware.getContentType(), savedFirmware.getContentType()); + Assert.assertEquals(firmware.getData(), savedFirmware.getData()); + + savedFirmware.setAdditionalInfo(JacksonUtil.newObjectNode()); + firmwareService.saveFirmware(savedFirmware); + + Firmware foundFirmware = firmwareService.findFirmwareById(tenantId, savedFirmware.getId()); + Assert.assertEquals(foundFirmware.getTitle(), savedFirmware.getTitle()); + + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } + + @Test + public void testSaveFirmwareInfoAndUpdateWithData() { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTenantId(tenantId); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + FirmwareInfo savedFirmwareInfo = firmwareService.saveFirmwareInfo(firmwareInfo); + + Assert.assertNotNull(savedFirmwareInfo); + Assert.assertNotNull(savedFirmwareInfo.getId()); + Assert.assertTrue(savedFirmwareInfo.getCreatedTime() > 0); + Assert.assertEquals(firmwareInfo.getTenantId(), savedFirmwareInfo.getTenantId()); + Assert.assertEquals(firmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); + + Firmware firmware = new Firmware(savedFirmwareInfo.getId()); + firmware.setCreatedTime(firmwareInfo.getCreatedTime()); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + + firmwareService.saveFirmware(firmware); + + savedFirmwareInfo.setAdditionalInfo(JacksonUtil.newObjectNode()); + firmwareService.saveFirmwareInfo(savedFirmwareInfo); + + Firmware foundFirmware = firmwareService.findFirmwareById(tenantId, firmware.getId()); + firmware.setAdditionalInfo(JacksonUtil.newObjectNode()); + + Assert.assertEquals(foundFirmware.getTitle(), firmware.getTitle()); + Assert.assertTrue(foundFirmware.isHasData()); + + firmwareService.deleteFirmware(tenantId, savedFirmwareInfo.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyTenant() { + Firmware firmware = new Firmware(); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyTitle() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyFileName() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyContentType() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyData() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithInvalidTenant() { + Firmware firmware = new Firmware(); + firmware.setTenantId(new TenantId(Uuids.timeBased())); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithEmptyChecksum() { + Firmware firmware = new Firmware(); + firmware.setTenantId(new TenantId(Uuids.timeBased())); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareInfoWithExistingTitleAndVersion() { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTenantId(tenantId); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION); + firmwareService.saveFirmwareInfo(firmwareInfo); + + FirmwareInfo newFirmwareInfo = new FirmwareInfo(); + newFirmwareInfo.setTenantId(tenantId); + newFirmwareInfo.setTitle(TITLE); + newFirmwareInfo.setVersion(VERSION); + firmwareService.saveFirmwareInfo(newFirmwareInfo); + } + + @Test(expected = DataValidationException.class) + public void testSaveFirmwareWithExistingTitleAndVersion() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + + Firmware newFirmware = new Firmware(); + newFirmware.setTenantId(tenantId); + newFirmware.setTitle(TITLE); + newFirmware.setVersion(VERSION); + newFirmware.setFileName(FILE_NAME); + newFirmware.setContentType(CONTENT_TYPE); + newFirmware.setData(DATA); + firmwareService.saveFirmware(newFirmware); + } + + @Test(expected = DataValidationException.class) + public void testDeleteFirmwareWithReferenceByDevice() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("My device"); + device.setType("default"); + device.setFirmwareId(savedFirmware.getId()); + Device savedDevice = deviceService.saveDevice(device); + + try { + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } finally { + deviceService.deleteDevice(tenantId, savedDevice.getId()); + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } + } + + @Test(expected = DataValidationException.class) + public void testDeleteFirmwareWithReferenceByDeviceProfile() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); + deviceProfile.setFirmwareId(savedFirmware.getId()); + DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + + try { + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } finally { + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } + } + + @Test + public void testFindFirmwareById() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + Firmware foundFirmware = firmwareService.findFirmwareById(tenantId, savedFirmware.getId()); + Assert.assertNotNull(foundFirmware); + Assert.assertEquals(savedFirmware, foundFirmware); + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } + + @Test + public void testFindFirmwareInfoById() { + FirmwareInfo firmware = new FirmwareInfo(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + FirmwareInfo savedFirmware = firmwareService.saveFirmwareInfo(firmware); + + FirmwareInfo foundFirmware = firmwareService.findFirmwareInfoById(tenantId, savedFirmware.getId()); + Assert.assertNotNull(foundFirmware); + Assert.assertEquals(savedFirmware, foundFirmware); + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + } + + @Test + public void testDeleteFirmware() { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + Firmware savedFirmware = firmwareService.saveFirmware(firmware); + + Firmware foundFirmware = firmwareService.findFirmwareById(tenantId, savedFirmware.getId()); + Assert.assertNotNull(foundFirmware); + firmwareService.deleteFirmware(tenantId, savedFirmware.getId()); + foundFirmware = firmwareService.findFirmwareById(tenantId, savedFirmware.getId()); + Assert.assertNull(foundFirmware); + } + + @Test + public void testFindTenantFirmwaresByTenantId() { + List firmwares = new ArrayList<>(); + for (int i = 0; i < 165; i++) { + Firmware firmware = new Firmware(); + firmware.setTenantId(tenantId); + firmware.setTitle(TITLE); + firmware.setVersion(VERSION + i); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + + FirmwareInfo info = new FirmwareInfo(firmwareService.saveFirmware(firmware)); + info.setHasData(true); + firmwares.add(info); + } + + List loadedFirmwares = new ArrayList<>(); + PageLink pageLink = new PageLink(16); + PageData pageData; + do { + pageData = firmwareService.findTenantFirmwaresByTenantId(tenantId, pageLink); + loadedFirmwares.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(firmwares, idComparator); + Collections.sort(loadedFirmwares, idComparator); + + Assert.assertEquals(firmwares, loadedFirmwares); + + firmwareService.deleteFirmwaresByTenantId(tenantId); + + pageLink = new PageLink(31); + pageData = firmwareService.findTenantFirmwaresByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + } + + @Test + public void testFindTenantFirmwaresByTenantIdAndHasData() { + List firmwares = new ArrayList<>(); + for (int i = 0; i < 165; i++) { + FirmwareInfo firmwareInfo = new FirmwareInfo(); + firmwareInfo.setTenantId(tenantId); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(VERSION + i); + firmwares.add(firmwareService.saveFirmwareInfo(firmwareInfo)); + } + + List loadedFirmwares = new ArrayList<>(); + PageLink pageLink = new PageLink(16); + PageData pageData; + do { + pageData = firmwareService.findTenantFirmwaresByTenantIdAndHasData(tenantId, false, pageLink); + loadedFirmwares.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(firmwares, idComparator); + Collections.sort(loadedFirmwares, idComparator); + + Assert.assertEquals(firmwares, loadedFirmwares); + + firmwares.forEach(f -> { + Firmware firmware = new Firmware(f.getId()); + firmware.setCreatedTime(f.getCreatedTime()); + firmware.setTenantId(f.getTenantId()); + firmware.setTitle(f.getTitle()); + firmware.setVersion(f.getVersion()); + firmware.setFileName(FILE_NAME); + firmware.setContentType(CONTENT_TYPE); + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); + firmware.setChecksum(CHECKSUM); + firmware.setData(DATA); + firmwareService.saveFirmware(firmware); + f.setHasData(true); + }); + + loadedFirmwares = new ArrayList<>(); + pageLink = new PageLink(16); + do { + pageData = firmwareService.findTenantFirmwaresByTenantIdAndHasData(tenantId, true, pageLink); + loadedFirmwares.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(firmwares, idComparator); + Collections.sort(loadedFirmwares, idComparator); + + Assert.assertEquals(firmwares, loadedFirmwares); + + firmwareService.deleteFirmwaresByTenantId(tenantId); + + pageLink = new PageLink(31); + pageData = firmwareService.findTenantFirmwaresByTenantId(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertTrue(pageData.getData().isEmpty()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/FirmwareServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/FirmwareServiceSqlTest.java new file mode 100644 index 0000000000..a89414e3a2 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/FirmwareServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseFirmwareServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class FirmwareServiceSqlTest extends BaseFirmwareServiceTest { +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 36d73a96ca..6760f96631 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -36,6 +36,9 @@ caffeine.specs.tenantProfiles.maxSize=100000 caffeine.specs.deviceProfiles.timeToLiveInMinutes=1440 caffeine.specs.deviceProfiles.maxSize=100000 +caffeine.specs.firmwares.timeToLiveInMinutes=1440 +caffeine.specs.firmwares.maxSize=100000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index a548ceec30..8297553ea6 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -29,4 +29,5 @@ DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; +DROP TABLE IF EXISTS firmware; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 333ce03fba..fbc0c76391 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -30,3 +30,4 @@ DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; DROP TABLE IF EXISTS resource; +DROP TABLE IF EXISTS firmware; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 211cebb2ad..950d3b2722 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -28,4 +28,6 @@ DROP TABLE IF EXISTS tb_schema_settings; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; -DROP TABLE IF EXISTS api_usage_state; \ No newline at end of file +DROP TABLE IF EXISTS api_usage_state; +DROP TABLE IF EXISTS resource; +DROP TABLE IF EXISTS firmware; \ No newline at end of file diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 0862e10a76..2d9b9808ed 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -74,6 +74,7 @@ import { StringOperation } from '@shared/models/query/query.models'; import { alarmFields } from '@shared/models/alarm.models'; +import { FirmwareService } from '@core/http/firmware.service'; @Injectable({ providedIn: 'root' @@ -93,6 +94,7 @@ export class EntityService { private dashboardService: DashboardService, private entityRelationService: EntityRelationService, private attributeService: AttributeService, + private firmwareService: FirmwareService, private utils: UtilsService ) { } @@ -128,6 +130,9 @@ export class EntityService { case EntityType.ALARM: console.error('Get Alarm Entity is not implemented!'); break; + case EntityType.FIRMWARE: + observable = this.firmwareService.getFirmwareInfo(entityId, config); + break; } return observable; } @@ -326,6 +331,10 @@ export class EntityService { case EntityType.ALARM: console.error('Get Alarm Entities is not implemented!'); break; + case EntityType.FIRMWARE: + pageLink.sortOrder.property = 'title'; + entitiesObservable = this.firmwareService.getFirmwares(pageLink, true, config); + break; } return entitiesObservable; } diff --git a/ui-ngx/src/app/core/http/firmware.service.ts b/ui-ngx/src/app/core/http/firmware.service.ts new file mode 100644 index 0000000000..7bf42fb49e --- /dev/null +++ b/ui-ngx/src/app/core/http/firmware.service.ts @@ -0,0 +1,122 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, defaultHttpUploadOptions, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { Firmware, FirmwareInfo } from '@shared/models/firmware.models'; +import { catchError, map, mergeMap } from 'rxjs/operators'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; + +@Injectable({ + providedIn: 'root' +}) +export class FirmwareService { + constructor( + private http: HttpClient + ) { + + } + + public getFirmwares(pageLink: PageLink, hasData?: boolean, config?: RequestConfig): Observable> { + let url = `/api/firmwares`; + if (isDefinedAndNotNull(hasData)) { + url += `/${hasData}`; + } + url += `${pageLink.toQuery()}`; + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + + public getFirmware(firmwareId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config)); + } + + public getFirmwareInfo(firmwareId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/firmware/info/${firmwareId}`, defaultHttpOptionsFromConfig(config)); + } + + public downloadFirmware(firmwareId: string): Observable { + return this.http.get(`/api/firmware/${firmwareId}/download`, { responseType: 'arraybuffer', observe: 'response' }).pipe( + map((response) => { + const headers = response.headers; + const filename = headers.get('x-filename'); + const contentType = headers.get('content-type'); + const linkElement = document.createElement('a'); + try { + const blob = new Blob([response.body], { type: contentType }); + const url = URL.createObjectURL(blob); + linkElement.setAttribute('href', url); + linkElement.setAttribute('download', filename); + const clickEvent = new MouseEvent('click', + { + view: window, + bubbles: true, + cancelable: false + } + ); + linkElement.dispatchEvent(clickEvent); + return null; + } catch (e) { + throw e; + } + }) + ); + } + + public saveFirmware(firmware: Firmware, config?: RequestConfig): Observable { + if (!firmware.file) { + return this.saveFirmwareInfo(firmware, config); + } + const firmwareInfo = deepClone(firmware); + delete firmwareInfo.file; + delete firmwareInfo.checksum; + delete firmwareInfo.checksumAlgorithm; + return this.saveFirmwareInfo(firmwareInfo, config).pipe( + mergeMap(res => { + return this.uploadFirmwareFile(res.id.id, firmware.file, firmware.checksumAlgorithm, firmware.checksum).pipe( + catchError(() => this.deleteFirmware(res.id.id)) + ); + }) + ); + } + + public saveFirmwareInfo(firmware: FirmwareInfo, config?: RequestConfig): Observable { + return this.http.post('/api/firmware', firmware, defaultHttpOptionsFromConfig(config)); + } + + public uploadFirmwareFile(firmwareId: string, file: File, checksumAlgorithm?: string, + checksum?: string, config?: RequestConfig): Observable { + if (!config) { + config = {}; + } + const formData = new FormData(); + formData.append('file', file); + let url = `/api/firmware/${firmwareId}`; + if (checksumAlgorithm && checksum) { + url += `?checksumAlgorithm=${checksumAlgorithm}&checksum=${checksum}`; + } + return this.http.post(url, formData, + defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest)); + } + + public deleteFirmware(firmwareId: string, config?: RequestConfig) { + return this.http.delete(`/api/firmware/${firmwareId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index 69f9912828..5927e59d45 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -39,3 +39,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false, params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) }; } + +export function defaultHttpUploadOptions(ignoreLoading: boolean = false, + ignoreErrors: boolean = false, + resendRequest: boolean = false) { + return { + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + }; +} diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index d8bc18291b..1b0796a389 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { Resource, ResourceInfo } from '@shared/models/resource.models'; -import { map } from 'rxjs/operators'; +import { catchError, map, mergeMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -70,6 +70,25 @@ export class ResourceService { ); } + public saveResources(resources: Resource[], config?: RequestConfig) { + let partSize = 100; + partSize = resources.length > partSize ? partSize : resources.length; + const resourceObservables = []; + for (let i = 0; i < partSize; i++) { + resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(error => of(error)))); + } + return forkJoin(resourceObservables).pipe( + mergeMap((resource) => { + resources.splice(0, partSize); + if (resources.length) { + return this.saveResources(resources, config); + } else { + return of(resource); + } + }) + ); + } + public saveResource(resource: Resource, config?: RequestConfig): Observable { return this.http.post('/api/resource', resource, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 7783fdd2c3..b14478f7e4 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -279,6 +279,13 @@ export class MenuService { icon: 'mdi:alpha-d-box', isMdiIcon: true }, + { + id: guid(), + name: 'firmware.firmware', + type: 'link', + path: '/firmwares', + icon: 'memory' + }, { id: guid(), name: 'entity-view.entity-views', @@ -379,6 +386,11 @@ export class MenuService { icon: 'mdi:alpha-d-box', isMdiIcon: true, path: '/deviceProfiles' + }, + { + name: 'firmware.firmware', + icon: 'memory', + path: '/firmwares' } ] }, diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b9c2697d0a..17c769e5ae 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -407,7 +407,7 @@ export function sortObjectKeys(obj: T): T { } export function deepTrim(obj: T): T { - if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null) { + if (isNumber(obj) || isUndefined(obj) || isString(obj) || obj === null || obj instanceof File) { return obj; } return Object.keys(obj).reduce((acc, curr) => { diff --git a/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html new file mode 100644 index 0000000000..4e733b3132 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.html @@ -0,0 +1,54 @@ + + + + + + + + + +
+
+ firmware.no-firmware-text +
+ + + {{ translate.get('firmware.no-firmware-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + +
+
+
+ + {{ requiredErrorText | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts new file mode 100644 index 0000000000..79dc170c7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/firmware/firmware-autocomplete.component.ts @@ -0,0 +1,236 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { map, mergeMap, share, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { BaseData } from '@shared/models/base-data'; +import { EntityService } from '@core/http/entity.service'; +import { TruncatePipe } from '@shared/pipe/truncate.pipe'; +import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { FirmwareInfo } from '@shared/models/firmware.models'; +import { FirmwareService } from '@core/http/firmware.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; + +@Component({ + selector: 'tb-firmware-autocomplete', + templateUrl: './firmware-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FirmwareAutocompleteComponent), + multi: true + }] +}) +export class FirmwareAutocompleteComponent implements ControlValueAccessor, OnInit { + + firmwareFormGroup: FormGroup; + + modelValue: string | null; + + @Input() + labelText: string; + + @Input() + requiredText: string; + + @Input() + useFullEntityId = false; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @ViewChild('firmwareInput', {static: true}) firmwareInput: ElementRef; + @ViewChild('firmwareInput', {read: MatAutocompleteTrigger}) firmwareAutocomplete: MatAutocompleteTrigger; + + filteredFirmwares: Observable>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private entityService: EntityService, + private firmwareService: FirmwareService, + private fb: FormBuilder) { + this.firmwareFormGroup = this.fb.group({ + firmwareId: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredFirmwares = this.firmwareFormGroup.get('firmwareId').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = this.useFullEntityId ? value.id : value.id.id; + } + this.updateView(modelValue); + if (value === null) { + this.clear(); + } + }), + map(value => value ? (typeof value === 'string' ? value : value.title) : ''), + mergeMap(name => this.fetchFirmware(name)), + share() + ); + } + + ngAfterViewInit(): void { + } + + getCurrentEntity(): BaseData | null { + const currentRuleChain = this.firmwareFormGroup.get('firmwareId').value; + if (currentRuleChain && typeof currentRuleChain !== 'string') { + return currentRuleChain as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.firmwareFormGroup.disable({emitEvent: false}); + } else { + this.firmwareFormGroup.enable({emitEvent: false}); + } + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + writeValue(value: string | EntityId | null): void { + this.searchText = ''; + if (value != null && value !== '') { + let firmwareId = ''; + if (typeof value === 'string') { + firmwareId = value; + } else if (value.entityType && value.id) { + firmwareId = value.id; + } + if (firmwareId !== '') { + this.entityService.getEntity(EntityType.FIRMWARE, firmwareId, {ignoreLoading: true, ignoreErrors: true}).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.firmwareFormGroup.get('firmwareId').patchValue(entity, {emitEvent: false}); + }, + () => { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + if (value !== null) { + this.propagateChange(this.modelValue); + } + } + ); + } else { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + } else { + this.modelValue = null; + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.firmwareFormGroup.get('firmwareId').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + reset() { + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: false}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayFirmwareFn(firmware?: FirmwareInfo): string | undefined { + return firmware ? `${firmware.title} (${firmware.version})` : undefined; + } + + fetchFirmware(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'title', + direction: Direction.ASC + }); + return this.firmwareService.getFirmwares(pageLink, true, {ignoreLoading: true}).pipe( + map((data) => data && data.data.length ? data.data : null) + ); + } + + clear() { + this.firmwareFormGroup.get('firmwareId').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.firmwareInput.nativeElement.blur(); + this.firmwareInput.nativeElement.focus(); + }, 0); + } + + get placeholderText(): string { + return this.labelText || 'firmware.firmware'; + } + + get requiredErrorText(): string { + return this.requiredText || 'firmware.firmware-required'; + } + + firmwareTitleText(firmware: FirmwareInfo): string { + return `${firmware.title} (${firmware.version})`; + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 950ee285e5..ab707b6b07 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -134,6 +134,7 @@ import { DashboardStateDialogComponent } from '@home/components/dashboard-page/s import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; +import { FirmwareAutocompleteComponent } from '@home/components/firmware/firmware-autocomplete.component'; @NgModule({ declarations: @@ -247,7 +248,8 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag ManageDashboardStatesDialogComponent, DashboardStateDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + FirmwareAutocompleteComponent ], imports: [ CommonModule, @@ -351,7 +353,8 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag ManageDashboardStatesDialogComponent, DashboardStateDialogComponent, EmbedDashboardDialogComponent, - DisplayWidgetTypesPanelComponent + DisplayWidgetTypesPanelComponent, + FirmwareAutocompleteComponent ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index 23219f1074..a4f9fc3482 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -60,6 +60,10 @@ {{ 'device-profile.type-required' | translate }} + + device-profile.description diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index a13d15492c..83f248db92 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -48,7 +48,7 @@ import { MatHorizontalStepper } from '@angular/material/stepper'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { deepTrim } from '@core/utils'; -import {ServiceType} from "@shared/models/queue.models"; +import { ServiceType } from '@shared/models/queue.models'; export interface AddDeviceProfileDialogData { deviceProfileName: string; @@ -72,13 +72,13 @@ export class AddDeviceProfileDialogComponent extends entityType = EntityType; - deviceProfileTypes = Object.keys(DeviceProfileType); + deviceProfileTypes = Object.values(DeviceProfileType); deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; deviceTransportTypeHints = deviceTransportTypeHintMap; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -108,6 +108,7 @@ export class AddDeviceProfileDialogComponent extends type: [DeviceProfileType.DEFAULT, [Validators.required]], defaultRuleChainId: [null, []], defaultQueueName: ['', []], + firmwareId: [null], description: ['', []] } ); @@ -186,6 +187,7 @@ export class AddDeviceProfileDialogComponent extends transportType: this.transportConfigFormGroup.get('transportType').value, provisionType: deviceProvisionConfiguration.type, provisionDeviceKey, + firmwareId: this.deviceProfileDetailsFormGroup.get('firmwareId').value, description: this.deviceProfileDetailsFormGroup.get('description').value, profileData: { configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 9ee6201db6..5f7593a03b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -63,6 +63,10 @@ [queueType]="serviceType" formControlName="defaultQueueName"> + + device-profile.type diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts index 28ac93c2af..23f1bd511c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -53,11 +53,11 @@ export class DeviceProfileComponent extends EntityComponent { entityType = EntityType; - deviceProfileTypes = Object.keys(DeviceProfileType); + deviceProfileTypes = Object.values(DeviceProfileType); deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -109,6 +109,7 @@ export class DeviceProfileComponent extends EntityComponent { }), defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], defaultQueueName: [entity ? entity.defaultQueueName : '', []], + firmwareId: [entity ? entity.firmwareId : null], description: [entity ? entity.description : '', []], } ); @@ -184,6 +185,7 @@ export class DeviceProfileComponent extends EntityComponent { }}, {emitEvent: false}); this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false}); this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false}); + this.entityForm.patchValue({firmwareId: entity.firmwareId}, {emitEvent: false}); this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 245cb6503b..416b006499 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -48,6 +48,10 @@ device.label + + device-profile.transport-type diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 1cba31aa24..df925e4e21 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -70,7 +70,7 @@ export class DeviceWizardDialogComponent extends entityType = EntityType; - deviceTransportTypes = Object.keys(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -107,6 +107,7 @@ export class DeviceWizardDialogComponent extends this.deviceWizardFormGroup = this.fb.group({ name: ['', Validators.required], label: [''], + firmwareId: [null], gateway: [false], overwriteActivityTime: [false], transportType: [DeviceTransportType.DEFAULT, Validators.required], @@ -312,6 +313,7 @@ export class DeviceWizardDialogComponent extends const device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, + firmwareId: this.deviceWizardFormGroup.get('firmwareId').value, deviceProfileId: profileId, additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index e1e3675e89..e6de5dbf84 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -172,7 +172,7 @@ export class EntityTableConfig, P extends PageLink = P deleteEntityContent: EntityStringFunction = () => ''; deleteEntitiesTitle: EntityCountStringFunction = () => ''; deleteEntitiesContent: EntityCountStringFunction = () => ''; - loadEntity: EntityByIdOperation = () => of(); + loadEntity: EntityByIdOperation = () => of(); saveEntity: EntityTwoWayOperation = (entity) => of(entity); deleteEntity: EntityIdOneWayOperation = () => of(); entitiesFetchFunction: EntitiesFetchFunction = () => of(emptyPageData()); diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index b01aced285..869b6a0c0a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -95,6 +95,10 @@ device.label + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 261bb1eba5..699bfd2b4e 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -79,6 +79,7 @@ export class DeviceComponent extends EntityComponent { { name: [entity ? entity.name : '', [Validators.required]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], + firmwareId: [entity ? entity.firmwareId : null], label: [entity ? entity.label : ''], deviceData: [entity ? entity.deviceData : null, [Validators.required]], additionalInfo: this.fb.group( @@ -95,6 +96,7 @@ export class DeviceComponent extends EntityComponent { updateForm(entity: DeviceInfo) { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({deviceProfileId: entity.deviceProfileId}); + this.entityForm.patchValue({firmwareId: entity.firmwareId}); this.entityForm.patchValue({label: entity.label}); this.entityForm.patchValue({deviceData: entity.deviceData}); this.entityForm.patchValue({ diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmware-routing.module.ts b/ui-ngx/src/app/modules/home/pages/firmware/firmware-routing.module.ts new file mode 100644 index 0000000000..688f3bfc1f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmware-routing.module.ts @@ -0,0 +1,48 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { RouterModule, Routes } from '@angular/router'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { NgModule } from '@angular/core'; +import { FirmwareTableConfigResolve } from '@home/pages/firmware/firmware-table-config.resolve'; + +const routes: Routes = [ + { + path: 'firmwares', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'firmware.firmware', + breadcrumb: { + label: 'firmware.firmware', + icon: 'memory' + } + }, + resolve: { + entitiesTableConfig: FirmwareTableConfigResolve + } + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + FirmwareTableConfigResolve + ] +}) +export class FirmwareRoutingModule{ } diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmware-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/firmware/firmware-table-config.resolve.ts new file mode 100644 index 0000000000..8fd2a8fccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmware-table-config.resolve.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { Firmware, FirmwareInfo } from '@shared/models/firmware.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { FirmwareService } from '@core/http/firmware.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { DeviceInfo } from '@shared/models/device.models'; + +@Injectable() +export class FirmwareTableConfigResolve implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private translate: TranslateService, + private datePipe: DatePipe, + private firmwareService: FirmwareService) { + this.config.entityType = EntityType.FIRMWARE; + this.config.entityComponent = FirmwaresComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.FIRMWARE); + this.config.entityResources = entityTypeResources.get(EntityType.FIRMWARE); + + this.config.entityTitle = (firmware) => firmware ? firmware.title : ''; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'firmware.title', '50%'), + new EntityTableColumn('version', 'firmware.version', '50%') + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('firmware.export'), + icon: 'file_download', + isEnabled: (firmware) => firmware.hasData, + onAction: ($event, entity) => this.exportFirmware($event, entity) + } + ); + + this.config.deleteEntityTitle = firmware => this.translate.instant('firmware.delete-firmware-title', + { firmwareTitle: firmware.title }); + this.config.deleteEntityContent = () => this.translate.instant('firmware.delete-firmware-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('firmware.delete-firmwares-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('firmware.delete-firmwares-text'); + + this.config.entitiesFetchFunction = pageLink => this.firmwareService.getFirmwares(pageLink); + this.config.loadEntity = id => this.firmwareService.getFirmwareInfo(id.id); + this.config.saveEntity = firmware => this.firmwareService.saveFirmware(firmware); + this.config.deleteEntity = id => this.firmwareService.deleteFirmware(id.id); + + this.config.onEntityAction = action => this.onFirmwareAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('firmware.firmware'); + return this.config; + } + + exportFirmware($event: Event, firmware: FirmwareInfo) { + if ($event) { + $event.stopPropagation(); + } + this.firmwareService.downloadFirmware(firmware.id.id).subscribe(); + } + + onFirmwareAction(action: EntityAction): boolean { + switch (action.action) { + case 'uploadFirmware': + this.exportFirmware(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmware.module.ts b/ui-ngx/src/app/modules/home/pages/firmware/firmware.module.ts new file mode 100644 index 0000000000..ab1b8343b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmware.module.ts @@ -0,0 +1,35 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; +import { FirmwareRoutingModule } from '@home/pages/firmware/firmware-routing.module'; +import { FirmwaresComponent } from '@home/pages/firmware/firmwares.component'; + +@NgModule({ + declarations: [ + FirmwaresComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + FirmwareRoutingModule + ] +}) +export class FirmwareModule { } diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html new file mode 100644 index 0000000000..c85ac61f0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.html @@ -0,0 +1,97 @@ + +
+ + +
+ +
+
+
+
+
+ firmware.warning-after-save-no-edit +
+ + firmware.title + + + {{ 'firmware.title-required' | translate }} + + + + firmware.version + + + {{ 'firmware.version-required' | translate }} + + +
+
+
+ + firmware.checksum-algorithm + + + + {{ checksumAlgorithmTranslationMap.get(checksumAlgorithm) }} + + + + + firmware.checksum + + + {{ 'firmware.checksum-required' | translate }} + + +
+ + +
+
+ + firmware.description + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts new file mode 100644 index 0000000000..78d04ecf53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/firmware/firmwares.component.ts @@ -0,0 +1,124 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { ChecksumAlgorithm, ChecksumAlgorithmTranslationMap, Firmware } from '@shared/models/firmware.models'; +import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; + +@Component({ + selector: 'tb-firmware', + templateUrl: './firmwares.component.html' +}) +export class FirmwaresComponent extends EntityComponent implements OnInit, OnDestroy { + + private destroy$ = new Subject(); + + checksumAlgorithms = Object.values(ChecksumAlgorithm); + checksumAlgorithmTranslationMap = ChecksumAlgorithmTranslationMap; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: Firmware, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + ngOnInit() { + super.ngOnInit(); + if (this.isAdd) { + this.entityForm.get('checksumAlgorithm').valueChanges.pipe( + map(algorithm => !!algorithm), + distinctUntilChanged(), + takeUntil(this.destroy$) + ).subscribe( + setAlgorithm => { + if (setAlgorithm) { + this.entityForm.get('checksum').setValidators([Validators.maxLength(1020), Validators.required]); + } else { + this.entityForm.get('checksum').clearValidators(); + } + this.entityForm.get('checksum').updateValueAndValidity({emitEvent: false}); + } + ); + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: Firmware): FormGroup { + const form = this.fb.group({ + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], + version: [entity ? entity.version : '', [Validators.required, Validators.maxLength(255)]], + additionalInfo: this.fb.group( + { + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + } + ) + }); + if (this.isAdd) { + form.addControl('checksumAlgorithm', this.fb.control(null)); + form.addControl('checksum', this.fb.control('', Validators.maxLength(1020))); + form.addControl('file', this.fb.control(null, Validators.required)); + } + return form; + } + + updateForm(entity: Firmware) { + if (this.isEdit) { + this.entityForm.get('title').disable({emitEvent: false}); + this.entityForm.get('version').disable({emitEvent: false}); + } + this.entityForm.patchValue({ + title: entity.title, + version: entity.version, + additionalInfo: { + description: entity.additionalInfo ? entity.additionalInfo.description : '' + } + }); + } + + onFirmwareIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('firmware.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index d823c36bc8..f1d9de02d4 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -35,6 +35,7 @@ import { modulesMap } from '../../common/modules-map'; import { DeviceProfileModule } from './device-profile/device-profile.module'; import { ApiUsageModule } from '@home/pages/api-usage/api-usage.module'; import { ResourceModule } from '@home/pages/resource/resource.module'; +import { FirmwareModule } from '@home/pages/firmware/firmware.module'; @NgModule({ exports: [ @@ -54,6 +55,7 @@ import { ResourceModule } from '@home/pages/resource/resource.module'; AuditLogModule, ApiUsageModule, ResourceModule, + FirmwareModule, UserModule ], providers: [ diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts index 55dc474a3d..bdff99ff06 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts @@ -85,10 +85,27 @@ export class ResourcesLibraryTableConfigResolver implements Resolve this.resourceService.getResources(pageLink) as Observable>; this.config.loadEntity = id => this.resourceService.getResource(id.id); - this.config.saveEntity = resource => this.resourceService.saveResource(resource); + this.config.saveEntity = resource => this.saveResource(resource); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); } + saveResource(resource) { + if (Array.isArray(resource.data)) { + const resources = []; + resource.data.forEach((data, index) => { + resources.push({ + resourceType: resource.resourceType, + data, + fileName: resource.fileName[index], + title: resource.title + }); + }); + return this.resourceService.saveResources(resources, {resendRequest: true}); + } else { + return this.resourceService.saveResource(resource); + } + } + resolve(): EntityTableConfig { this.config.tableTitle = this.translate.instant('resource.resources-library'); const authUser = getCurrentAuthUser(this.store); diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html index e4fa600f9c..60ac91b120 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html @@ -44,9 +44,11 @@ diff --git a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts index 560b8a3d43..5f7a35c5b9 100644 --- a/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts @@ -29,7 +29,7 @@ import { ResourceTypeMIMETypes, ResourceTypeTranslationMap } from '@shared/models/resource.models'; -import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; +import { pairwise, startWith, takeUntil } from 'rxjs/operators'; @Component({ selector: 'tb-resources-library', @@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent impleme ngOnInit() { super.ngOnInit(); this.entityForm.get('resourceType').valueChanges.pipe( - distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)), + startWith(ResourceType.LWM2M_MODEL), + pairwise(), takeUntil(this.destroy$) - ).subscribe((type) => { + ).subscribe(([previousType, type]) => { + if (previousType === this.resourceType.LWM2M_MODEL) { + this.entityForm.get('title').setValidators(Validators.required); + this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); + } if (type === this.resourceType.LWM2M_MODEL) { this.entityForm.get('title').clearValidators(); - } else { - this.entityForm.get('title').setValidators(Validators.required); + this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); } - this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); + this.entityForm.patchValue({ + data: null, + fileName: null + }, {emitEvent: false}); }); } @@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent impleme return '*/*'; } } + + convertToBase64File(data: string): string { + return window.btoa(data); + } } diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index ea0ae424bb..5f3263522c 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -18,7 +18,7 @@
+ [flowConfig]="{allowDuplicateUploads: true}">
diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index b91fba1def..d70837ac82 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -17,6 +17,7 @@ import { AfterViewInit, Component, + ElementRef, EventEmitter, forwardRef, Input, @@ -102,17 +103,38 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, existingFileName: string; @Input() - convertToBase64 = false; + readAsBinary = false; + + @Input() + workFromFileObj = false; + + private multipleFileValue = false; + + @Input() + set multipleFile(value: boolean) { + this.multipleFileValue = value; + if (this.flow?.flowJs) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + get multipleFile(): boolean { + return this.multipleFileValue; + } @Output() - fileNameChanged = new EventEmitter(); + fileNameChanged = new EventEmitter(); - fileName: string; + fileName: string | string[]; fileContent: any; + files: File[]; @ViewChild('flow', {static: true}) flow: FlowDirective; + @ViewChild('flowInput', {static: true}) + flowInput: ElementRef; + autoUploadSubscription: Subscription; private propagateChange = null; @@ -125,34 +147,68 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, ngAfterViewInit() { this.autoUploadSubscription = this.flow.events$.subscribe(event => { - if (event.type === 'fileAdded') { - const file = event.event[0] as flowjs.FlowFile; - if (this.filterFile(file)) { - const reader = new FileReader(); - reader.onload = (loadEvent) => { - if (typeof reader.result === 'string') { - const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; - if (fileContent && fileContent.length > 0) { - if (this.contentConvertFunction) { - this.fileContent = this.contentConvertFunction(fileContent); - } else { - this.fileContent = fileContent; - } - if (this.fileContent) { - this.fileName = file.name; - } else { - this.fileName = null; - } - this.updateModel(); + if (event.type === 'filesAdded') { + const readers = []; + (event.event[0] as flowjs.FlowFile[]).forEach(file => { + if (this.filterFile(file)) { + readers.push(this.readerAsFile(file)); + } + }); + if (readers.length) { + Promise.all(readers).then((files) => { + files = files.filter(file => file.fileContent != null || file.files != null); + if (files.length === 1) { + this.fileContent = files[0].fileContent; + this.fileName = files[0].fileName; + this.files = files[0].files; + this.updateModel(); + } else if (files.length > 1) { + this.fileContent = files.map(content => content.fileContent); + this.fileName = files.map(content => content.fileName); + this.files = files.map(content => content.files); + this.updateModel(); + } + }); + } + } + }); + if (!this.multipleFile) { + this.updateMultipleFileMode(this.multipleFile); + } + } + + private readerAsFile(file: flowjs.FlowFile): Promise { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + let fileName = null; + let fileContent = null; + let files = null; + if (typeof reader.result === 'string') { + fileContent = reader.result; + if (fileContent && fileContent.length > 0) { + if (!this.workFromFileObj) { + if (this.contentConvertFunction) { + fileContent = this.contentConvertFunction(fileContent); + } + if (fileContent) { + fileName = file.name; } + } else { + files = file.file; + fileName = file.name; } - }; - if (this.convertToBase64) { - reader.readAsBinaryString(file.file); - } else { - reader.readAsText(file.file); } } + resolve({fileContent, fileName, files}); + }; + reader.onerror = () => { + resolve({fileContent: null, fileName: null, files: null}); + }; + if (this.readAsBinary) { + reader.readAsBinaryString(file.file); + } else { + reader.readAsText(file.file); } }); } @@ -183,7 +239,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } writeValue(value: any): void { - this.fileName = this.existingFileName || null; + let fileName = null; + if (this.workFromFileObj && value instanceof File) { + fileName = Array.isArray(value) ? value.map(file => file.name) : value.name; + } + this.fileName = this.existingFileName || fileName; } ngOnChanges(changes: SimpleChanges): void { @@ -198,13 +258,25 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } private updateModel() { - this.propagateChange(this.fileContent); - this.fileNameChanged.emit(this.fileName); + if (this.workFromFileObj) { + this.propagateChange(this.files); + } else { + this.propagateChange(this.fileContent); + this.fileNameChanged.emit(this.fileName); + } } clearFile() { this.fileName = null; this.fileContent = null; + this.files = null; this.updateModel(); } + + private updateMultipleFileMode(multiple: boolean) { + this.flow.flowJs.opts.singleFile = !multiple; + if (!multiple) { + this.flowInput.nativeElement.removeAttribute('multiple'); + } + } } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 1463c6e51b..904929c2b1 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -27,6 +27,7 @@ import { KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import * as _moment from 'moment'; import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { FirmwareId } from '@shared/models/id/firmware-id'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -445,6 +446,7 @@ export interface DeviceProfile extends BaseData { provisionDeviceKey?: string; defaultRuleChainId?: RuleChainId; defaultQueueName?: string; + firmwareId?: FirmwareId; profileData: DeviceProfileData; } @@ -499,6 +501,7 @@ export interface Device extends BaseData { name: string; type: string; label: string; + firmwareId?: FirmwareId; deviceProfileId?: DeviceProfileId; deviceData?: DeviceData; additionalInfo?: any; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 846aa4e6dc..2ee8b226cc 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -33,7 +33,8 @@ export enum EntityType { WIDGETS_BUNDLE = 'WIDGETS_BUNDLE', WIDGET_TYPE = 'WIDGET_TYPE', API_USAGE_STATE = 'API_USAGE_STATE', - TB_RESOURCE = 'TB_RESOURCE' + TB_RESOURCE = 'TB_RESOURCE', + FIRMWARE = 'FIRMWARE' } export enum AliasEntityType { @@ -278,6 +279,16 @@ export const entityTypeTranslations = new Map( + [ + [ChecksumAlgorithm.MD5, 'MD5'], + [ChecksumAlgorithm.SHA256, 'SHA-256'], + [ChecksumAlgorithm.CRC32, 'CRC-32'] + ] +); + +export interface FirmwareInfo extends BaseData { + tenantId?: TenantId; + title?: string; + version?: string; + hasData?: boolean; + additionalInfo?: any; +} + +export interface Firmware extends FirmwareInfo { + file?: File; + data: string; + fileName: string; + checksum?: ChecksumAlgorithm; + checksumAlgorithm?: string; + contentType: string; +} diff --git a/ui-ngx/src/app/shared/models/id/firmware-id.ts b/ui-ngx/src/app/shared/models/id/firmware-id.ts new file mode 100644 index 0000000000..31dd21210b --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/firmware-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class FirmwareId implements EntityId { + entityType = EntityType.FIRMWARE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 805a046673..154f3c65f9 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo { data: string; fileName: string; } + +export interface Resources extends ResourceInfo { + data: string|string[]; + fileName: string|string[]; +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1f5a6de39e..f8672eed56 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1693,6 +1693,36 @@ "inherit-owner": "Inherit from owner", "source-attribute-not-set": "If source attribute isn't set" }, + "firmware": { + "add": "Add firmware", + "checksum": "Checksum", + "checksum-required": "Checksum is required.", + "checksum-algorithm": "Checksum algorithm", + "copyId": "Copy firmware Id", + "description": "Description", + "delete": "Delete firmware", + "delete-firmware-text": "Be careful, after the confirmation the firmware will become unrecoverable.", + "delete-firmware-title": "Are you sure you want to delete the firmware '{{firmwareTitle}}'?", + "delete-firmwares-action-title": "Delete { count, plural, 1 {1 firmware} other {# firmwares} }", + "delete-firmwares-text": "Be careful, after the confirmation all selected resources will be removed.", + "delete-firmwares-title": "Are you sure you want to delete { count, plural, 1 {1 firmware} other {# firmwares} }?", + "drop-file": "Drop a firmware file or click to select a file to upload.", + "empty": "Firmware is empty", + "export": "Export firmware", + "idCopiedMessage": "Firmware Id has been copied to clipboard", + "no-firmware-matching": "No firmware matching '{{entity}}' were found.", + "no-firmware-text": "No firmwares found", + "firmware": "Firmware", + "firmware-details": "Firmware details", + "firmware-required": "Firmware is required.", + "search": "Search firmwares", + "selected-firmware": "{ count, plural, 1 {1 firmware} other {# firmwares} } selected", + "title": "Title", + "title-required": "Title is required.", + "version": "Version", + "version-required": "Version is required.", + "warning-after-save-no-edit": "Once the firmware is saved, it will not be possible to change the title and version fields." + }, "fullscreen": { "expand": "Expand to fullscreen", "exit": "Exit fullscreen",