From 085e3c1580054531e255316e051582edc548162d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 23 Sep 2022 15:37:17 +0300 Subject: [PATCH] Implement Asset Profile feature --- .../upgrade/3.4.1/schema_update_after.sql | 21 + .../upgrade/3.4.1/schema_update_before.sql | 45 ++ .../server/actors/ActorSystemContext.java | 5 + .../actors/ruleChain/DefaultTbContext.java | 6 + .../controller/AssetProfileController.java | 230 +++++++++ .../server/controller/BaseController.java | 25 + .../controller/ControllerConstants.java | 10 + .../controller/DeviceProfileController.java | 2 +- .../install/ThingsboardInstallService.java | 3 + .../service/asset/AssetBulkImportService.java | 12 + .../edge/DefaultEdgeNotificationService.java | 1 + .../service/edge/EdgeContextComponent.java | 8 + .../service/edge/rpc/EdgeGrpcSession.java | 8 + .../service/edge/rpc/EdgeSyncCursor.java | 2 + .../rpc/constructor/AssetMsgConstructor.java | 4 + .../AssetProfileMsgConstructor.java | 67 +++ .../fetch/AssetProfilesEdgeEventFetcher.java | 47 ++ .../processor/AssetProfileEdgeProcessor.java | 63 +++ .../edge/rpc/processor/BaseEdgeProcessor.java | 12 + .../rpc/processor/TelemetryEdgeProcessor.java | 21 +- .../rpc/sync/DefaultEdgeRequestsService.java | 50 ++ .../edge/rpc/sync/EdgeRequestsService.java | 3 + .../profile/DefaultTbAssetProfileService.java | 99 ++++ .../asset/profile/TbAssetProfileService.java | 26 + .../install/SqlDatabaseUpgradeService.java | 55 +++ .../profile/DefaultTbAssetProfileCache.java | 162 +++++++ .../service/profile/TbAssetProfileCache.java | 33 ++ .../queue/DefaultTbClusterService.java | 44 +- .../queue/DefaultTbCoreConsumerService.java | 4 +- .../DefaultTbRuleEngineConsumerService.java | 4 +- .../processing/AbstractConsumerService.java | 21 +- .../service/security/AccessValidator.java | 27 ++ .../permission/CustomerUserPermissions.java | 5 +- .../service/security/permission/Resource.java | 1 + .../permission/TenantAdminPermissions.java | 1 + .../DefaultEntitiesExportImportService.java | 2 +- .../DefaultExportableEntitiesService.java | 7 +- .../ie/exporting/impl/AssetExportService.java | 1 + .../impl/AssetProfileExportService.java | 43 ++ .../ie/importing/impl/AssetImportService.java | 1 + .../impl/AssetProfileImportService.java | 82 ++++ .../src/main/resources/thingsboard.yml | 4 + .../server/controller/AbstractWebTest.java | 10 + .../controller/BaseAssetControllerTest.java | 26 +- .../BaseAssetProfileControllerTest.java | 452 ++++++++++++++++++ .../controller/BaseEdgeControllerTest.java | 7 +- .../sql/AssetProfileControllerSqlTest.java | 23 + .../thingsboard/server/edge/BaseEdgeTest.java | 2 +- .../server/edge/imitator/EdgeImitator.java | 6 + .../sync/ie/BaseExportImportServiceTest.java | 24 +- .../sync/ie/ExportImportServiceSqlTest.java | 97 ++-- .../server/dao/asset/AssetProfileService.java | 53 ++ .../server/dao/asset/AssetService.java | 7 +- .../server/common/data/CacheConstants.java | 2 + .../server/common/data/EdgeUtils.java | 2 + .../server/common/data/EntityType.java | 2 +- .../server/common/data/asset/Asset.java | 20 +- .../server/common/data/asset/AssetInfo.java | 11 +- .../common/data/asset/AssetProfile.java | 118 +++++ .../common/data/asset/AssetProfileInfo.java | 62 +++ .../common/data/edge/EdgeEventType.java | 1 + .../server/common/data/id/AssetProfileId.java | 43 ++ .../common/data/id/EntityIdFactory.java | 4 + .../server/common/data/sync/JsonTbEntity.java | 2 + common/edge-api/src/main/proto/edge.proto | 30 +- .../server/dao/asset/AssetDao.java | 33 ++ .../dao/asset/AssetProfileCacheKey.java | 63 +++ .../dao/asset/AssetProfileCaffeineCache.java | 33 ++ .../server/dao/asset/AssetProfileDao.java | 46 ++ .../dao/asset/AssetProfileEvictEvent.java | 31 ++ .../dao/asset/AssetProfileRedisCache.java | 35 ++ .../dao/asset/AssetProfileServiceImpl.java | 286 +++++++++++ .../server/dao/asset/BaseAssetService.java | 44 ++ .../server/dao/edge/EdgeServiceImpl.java | 1 + .../server/dao/entity/BaseEntityService.java | 1 + .../server/dao/model/ModelConstants.java | 15 + .../dao/model/sql/AbstractAssetEntity.java | 11 + .../server/dao/model/sql/AssetInfoEntity.java | 8 +- .../dao/model/sql/AssetProfileEntity.java | 136 ++++++ .../service/validator/AssetDataValidator.java | 3 - .../validator/AssetProfileDataValidator.java | 109 +++++ .../dao/sql/asset/AssetProfileRepository.java | 63 +++ .../server/dao/sql/asset/AssetRepository.java | 62 ++- .../server/dao/sql/asset/JpaAssetDao.java | 36 ++ .../dao/sql/asset/JpaAssetProfileDao.java | 126 +++++ .../query/DefaultEntityQueryRepository.java | 1 + .../dao/sql/query/EntityKeyMapping.java | 1 + .../server/dao/tenant/TenantServiceImpl.java | 6 + .../main/resources/sql/schema-entities.sql | 51 +- .../dao/service/AbstractServiceTest.java | 15 + .../service/BaseAssetProfileServiceTest.java | 279 +++++++++++ .../dao/service/BaseAssetServiceTest.java | 160 ++++++- .../sql/AssetProfileServiceSqlTest.java | 23 + .../server/dao/sql/asset/JpaAssetDaoTest.java | 44 +- .../resources/application-test.properties | 3 + .../resources/sql/psql/drop-all-tables.sql | 1 + .../test/resources/sql/system-test-psql.sql | 2 +- dao/src/test/resources/sql/system-test.sql | 2 + .../sql/timescale/drop-all-tables.sql | 1 + .../api/RuleEngineAssetProfileCache.java | 40 ++ .../rule/engine/api/TbContext.java | 2 + .../tools/migrator/RelatedEntitiesParser.java | 1 + 102 files changed, 3925 insertions(+), 120 deletions(-) create mode 100644 application/src/main/data/upgrade/3.4.1/schema_update_after.sql create mode 100644 application/src/main/data/upgrade/3.4.1/schema_update_before.sql create mode 100644 application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetProfileServiceSqlTest.java create mode 100644 rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAssetProfileCache.java diff --git a/application/src/main/data/upgrade/3.4.1/schema_update_after.sql b/application/src/main/data/upgrade/3.4.1/schema_update_after.sql new file mode 100644 index 0000000000..78df4d2fd2 --- /dev/null +++ b/application/src/main/data/upgrade/3.4.1/schema_update_after.sql @@ -0,0 +1,21 @@ +-- +-- Copyright © 2016-2022 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +DROP PROCEDURE IF EXISTS update_asset_profiles; + +ALTER TABLE asset ALTER COLUMN asset_profile_id SET NOT NULL; +ALTER TABLE asset DROP CONSTRAINT IF EXISTS fk_asset_profile; +ALTER TABLE asset ADD CONSTRAINT fk_asset_profile FOREIGN KEY (asset_profile_id) REFERENCES asset_profile(id); diff --git a/application/src/main/data/upgrade/3.4.1/schema_update_before.sql b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql new file mode 100644 index 0000000000..94a7e1fad1 --- /dev/null +++ b/application/src/main/data/upgrade/3.4.1/schema_update_before.sql @@ -0,0 +1,45 @@ +-- +-- Copyright © 2016-2022 The Thingsboard Authors +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +CREATE TABLE IF NOT EXISTS asset_profile ( + id uuid NOT NULL CONSTRAINT asset_profile_pkey PRIMARY KEY, + created_time bigint NOT NULL, + name varchar(255), + image varchar(1000000), + description varchar, + search_text varchar(255), + is_default boolean, + tenant_id uuid, + default_rule_chain_id uuid, + default_dashboard_id uuid, + default_queue_name varchar(255), + external_id uuid, + CONSTRAINT asset_profile_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT asset_profile_external_id_unq_key UNIQUE (tenant_id, external_id), + CONSTRAINT fk_default_rule_chain_asset_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id), + CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id) + ); + +CREATE OR REPLACE PROCEDURE update_asset_profiles() + LANGUAGE plpgsql AS +$$ +BEGIN +UPDATE asset as a SET asset_profile_id = p.id + FROM + (SELECT id, tenant_id, name from asset_profile) as p +WHERE a.asset_profile_id IS NULL AND p.tenant_id = a.tenant_id AND a.type = p.name; +END; +$$; diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b4c308db86..d028613c51 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -86,6 +86,7 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.executors.ExternalCallExecutorService; import org.thingsboard.server.service.executors.SharedEventLoopGroupService; import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbRpcService; @@ -182,6 +183,10 @@ public class ActorSystemContext { @Getter private TbDeviceProfileCache deviceProfileCache; + @Autowired + @Getter + private TbAssetProfileCache assetProfileCache; + @Autowired @Getter private AssetService assetService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 92fdfc6e48..b6c400685c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; @@ -533,6 +534,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getDeviceProfileCache(); } + @Override + public RuleEngineAssetProfileCache getAssetProfileCache() { + return mainCtx.getAssetProfileCache(); + } + @Override public EdgeService getEdgeService() { return mainCtx.getEdgeService(); diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java new file mode 100644 index 0000000000..a87ae9227c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -0,0 +1,230 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class AssetProfileController extends BaseController { + + private final TbAssetProfileService tbAssetProfileService; + + @Autowired + private TimeseriesService timeseriesService; + + @ApiOperation(value = "Get Asset Profile (getAssetProfileById)", + notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " + + "The server checks that the asset profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.GET) + @ResponseBody + public AssetProfile getAssetProfileById( + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + checkParameter(ASSET_PROFILE_ID, strAssetProfileId); + try { + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + return checkAssetProfileId(assetProfileId, Operation.READ); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)", + notes = "Fetch the Asset Profile Info object based on the provided Asset Profile Id. " + + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assetProfileInfo/{assetProfileId}", method = RequestMethod.GET) + @ResponseBody + public AssetProfileInfo getAssetProfileInfoById( + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + checkParameter(ASSET_PROFILE_ID, strAssetProfileId); + try { + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + return new AssetProfileInfo(checkAssetProfileId(assetProfileId, Operation.READ)); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Get Default Asset Profile (getDefaultAssetProfileInfo)", + notes = "Fetch the Default Asset Profile Info object. " + + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assetProfileInfo/default", method = RequestMethod.GET) + @ResponseBody + public AssetProfileInfo getDefaultAssetProfileInfo() throws ThingsboardException { + try { + return checkNotNull(assetProfileService.findDefaultAssetProfileInfo(getTenantId())); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Create Or Update Asset Profile (saveAssetProfile)", + notes = "Create or update the Asset Profile. When creating asset profile, platform generates asset profile id as " + UUID_WIKI_LINK + + "The newly created asset profile id will be present in the response. " + + "Specify existing asset profile id to update the asset profile. " + + "Referencing non-existing asset profile Id will cause 'Not Found' error. " + NEW_LINE + + "Asset profile name is unique in the scope of tenant. Only one 'default' asset profile may exist in scope of tenant. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Asset Profile entity. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json", + consumes = "application/json") + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/assetProfile", method = RequestMethod.POST) + @ResponseBody + public AssetProfile saveAssetProfile( + @ApiParam(value = "A JSON value representing the asset profile.") + @RequestBody AssetProfile assetProfile) throws Exception { + assetProfile.setTenantId(getTenantId()); + checkEntity(assetProfile.getId(), assetProfile, Resource.ASSET_PROFILE); + return tbAssetProfileService.save(assetProfile, getCurrentUser()); + } + + @ApiOperation(value = "Delete asset profile (deleteAssetProfile)", + notes = "Deletes the asset profile. Referencing non-existing asset profile Id will cause an error. " + + "Can't delete the asset profile if it is referenced by existing assets." + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteAssetProfile( + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + checkParameter(ASSET_PROFILE_ID, strAssetProfileId); + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.DELETE); + tbAssetProfileService.delete(assetProfile, getCurrentUser()); + } + + @ApiOperation(value = "Make Asset Profile Default (setDefaultAssetProfile)", + notes = "Marks asset profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/assetProfile/{assetProfileId}/default", method = RequestMethod.POST) + @ResponseBody + public AssetProfile setDefaultAssetProfile( + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException { + checkParameter(ASSET_PROFILE_ID, strAssetProfileId); + AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId)); + AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.WRITE); + AssetProfile previousDefaultAssetProfile = assetProfileService.findDefaultAssetProfile(getTenantId()); + return tbAssetProfileService.setDefaultAssetProfile(assetProfile, previousDefaultAssetProfile, getCurrentUser()); + } + + @ApiOperation(value = "Get Asset Profiles (getAssetProfiles)", + notes = "Returns a page of asset profile objects owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getAssetProfiles( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(assetProfileService.findAssetProfiles(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Get Asset Profile infos (getAssetProfileInfos)", + notes = "Returns a page of asset profile info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getAssetProfileInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } +} 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 1e9958cc25..21edd71ddd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -57,6 +57,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; @@ -65,6 +66,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -96,6 +98,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; @@ -132,6 +135,7 @@ import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.entitiy.TbNotificationEntityService; import org.thingsboard.server.service.ota.OtaPackageStateService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.security.model.SecurityUser; @@ -190,6 +194,9 @@ public abstract class BaseController { @Autowired protected AssetService assetService; + @Autowired + protected AssetProfileService assetProfileService; + @Autowired protected AlarmSubscriptionService alarmService; @@ -265,6 +272,9 @@ public abstract class BaseController { @Autowired protected TbDeviceProfileCache deviceProfileCache; + @Autowired + protected TbAssetProfileCache assetProfileCache; + @Autowired(required = false) protected EdgeService edgeService; @@ -548,6 +558,9 @@ public abstract class BaseController { case ASSET: checkAssetId(new AssetId(entityId.getId()), operation); return; + case ASSET_PROFILE: + checkAssetProfileId(new AssetProfileId(entityId.getId()), operation); + return; case DASHBOARD: checkDashboardId(new DashboardId(entityId.getId()), operation); return; @@ -667,6 +680,18 @@ public abstract class BaseController { } } + AssetProfile checkAssetProfileId(AssetProfileId assetProfileId, Operation operation) throws ThingsboardException { + try { + validateId(assetProfileId, "Incorrect assetProfileId " + assetProfileId); + AssetProfile assetProfile = assetProfileService.findAssetProfileById(getCurrentUser().getTenantId(), assetProfileId); + checkNotNull(assetProfile, "Asset profile with id [" + assetProfileId + "] is not found"); + accessControlService.checkPermission(getCurrentUser(), Resource.ASSET_PROFILE, operation, assetProfileId, assetProfile); + return assetProfile; + } catch (Exception e) { + throw handleException(e, false); + } + } + Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException { try { validateId(alarmId, "Incorrect alarmId " + alarmId); diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index d4199006d0..e00ccaa4bc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -35,6 +35,8 @@ public class ControllerConstants { protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + + protected static final String ASSET_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the asset profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; @@ -75,6 +77,8 @@ public class ControllerConstants { protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; + + protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the edge name."; protected static final String EVENT_TEXT_SEARCH_DESCRIPTION = "The value is not used in searching."; @@ -92,6 +96,8 @@ public class ControllerConstants { protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name"; protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email"; protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault"; + + protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault"; protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status"; protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id"; @@ -110,6 +116,8 @@ public class ControllerConstants { protected static final String RELATION_INFO_DESCRIPTION = "Relation Info is an extension of the default Relation object that contains information about the 'from' and 'to' entity names. "; protected static final String EDGE_INFO_DESCRIPTION = "Edge Info is an extension of the default Edge object that contains information about the assigned customer name. "; protected static final String DEVICE_PROFILE_INFO_DESCRIPTION = "Device Profile Info is a lightweight object that includes main information about Device Profile excluding the heavyweight configuration object. "; + + protected static final String ASSET_PROFILE_INFO_DESCRIPTION = "Asset Profile Info is a lightweight object that includes main information about Asset Profile. "; protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)"; protected static final String QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES = "TB-RULE-ENGINE, TB-CORE, TB-TRANSPORT, JS-EXECUTOR"; protected static final String QUEUE_QUEUE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the queue name."; @@ -1415,6 +1423,8 @@ public class ControllerConstants { protected static final String DEVICE_PROFILE_ID = "deviceProfileId"; + protected static final String ASSET_PROFILE_ID = "assetProfileId"; + protected static final String MODEL_DESCRIPTION = "See the 'Model' tab for more details."; protected static final String ENTITY_VIEW_DESCRIPTION = "Entity Views limit the degree of exposure of the Device or Asset telemetry and attributes to the Customers. " + "Every Entity View references exactly one entity (device or asset) and defines telemetry and attribute keys that will be visible to the assigned Customer. " + 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 0baf89e0dc..bd2a5a2ad9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -191,7 +191,7 @@ public class DeviceProfileController extends BaseController { "Specify existing device profile id to update the device profile. " + "Referencing non-existing device profile Id will cause 'Not Found' error. " + NEW_LINE + "Device profile name is unique in the scope of tenant. Only one 'default' device profile may exist in scope of tenant." + DEVICE_PROFILE_DATA + - "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device Profile entity. " + + "Remove 'id', 'tenantId' from the request body example (below) to create new Device Profile entity. " + TENANT_AUTHORITY_PARAGRAPH, produces = "application/json", consumes = "application/json") 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 2c3251834f..c1f352d8eb 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -225,6 +225,9 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.4.0 to 3.4.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.4.0"); dataUpdateService.updateData("3.4.0"); + case "3.4.1": + log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.4.1"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index ff89c3dd06..cbe5eac4ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -21,9 +21,13 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService; @@ -40,6 +44,7 @@ import java.util.Optional; public class AssetBulkImportService extends AbstractBulkImportService { private final AssetService assetService; private final TbAssetService tbAssetService; + private final AssetProfileService assetProfileService; @Override protected void setEntityFields(Asset entity, Map fields) { @@ -66,6 +71,13 @@ public class AssetBulkImportService extends AbstractBulkImportService { @Override @SneakyThrows protected Asset saveEntity(SecurityUser user, Asset entity, Map fields) { + AssetProfile assetProfile; + if (StringUtils.isNotEmpty(entity.getType())) { + assetProfile = assetProfileService.findOrCreateAssetProfile(entity.getTenantId(), entity.getType()); + } else { + assetProfile = assetProfileService.findDefaultAssetProfile(entity.getTenantId()); + } + entity.setAssetProfileId(assetProfile.getId()); return tbAssetService.save(entity, user); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index 5fa58a5bb5..23fb8fd4be 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -134,6 +134,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { case ASSET: case DEVICE: case DEVICE_PROFILE: + case ASSET_PROFILE: case ENTITY_VIEW: case DASHBOARD: case RULE_CHAIN: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 29ff49d626..8bc781b5fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -38,6 +39,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.processor.AdminSettingsEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.AlarmEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.AssetEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.AssetProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.CustomerEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.DeviceEdgeProcessor; @@ -83,6 +85,9 @@ public class EdgeContextComponent { @Autowired private DeviceProfileService deviceProfileService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired private AttributesService attributesService; @@ -113,6 +118,9 @@ public class EdgeContextComponent { @Autowired private DeviceProfileEdgeProcessor deviceProfileProcessor; + @Autowired + private AssetProfileEdgeProcessor assetProfileProcessor; + @Autowired private DeviceEdgeProcessor deviceProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 3f8950f281..5f1c03b35a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; @@ -513,6 +514,8 @@ public final class EdgeGrpcSession implements Closeable { return ctx.getDeviceProcessor().processDeviceToEdge(edge, edgeEvent, msgType, action); case DEVICE_PROFILE: return ctx.getDeviceProfileProcessor().processDeviceProfileToEdge(edgeEvent, msgType, action); + case ASSET_PROFILE: + return ctx.getAssetProfileProcessor().processAssetProfileToEdge(edgeEvent, msgType, action); case ASSET: return ctx.getAssetProcessor().processAssetToEdge(edge, edgeEvent, msgType, action); case ENTITY_VIEW: @@ -634,6 +637,11 @@ public final class EdgeGrpcSession implements Closeable { result.add(ctx.getEdgeRequestsService().processDeviceProfileDevicesRequestMsg(edge.getTenantId(), edge, deviceProfileDevicesRequestMsg)); } } + if (uplinkMsg.getAssetProfileAssetsRequestMsgCount() > 0) { + for (AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg : uplinkMsg.getAssetProfileAssetsRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processAssetProfileAssetsRequestMsg(edge.getTenantId(), edge, assetProfileAssetsRequestMsg)); + } + } if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) { for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) { result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 1df44dd589..d6bfff76f6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; @@ -47,6 +48,7 @@ public class EdgeSyncCursor { fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig())); fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); + fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { fetchers.add(new CustomerEdgeEventFetcher()); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java index 0a78afa6b7..fb84d7f85a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java @@ -42,6 +42,10 @@ public class AssetMsgConstructor { builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits()); builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits()); } + if (asset.getAssetProfileId() != null) { + builder.setAssetProfileIdMSB(asset.getAssetProfileId().getId().getMostSignificantBits()); + builder.setAssetProfileIdLSB(asset.getAssetProfileId().getId().getLeastSignificantBits()); + } if (asset.getAdditionalInfo() != null) { builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo())); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java new file mode 100644 index 0000000000..78f7b0327e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.nio.charset.StandardCharsets; + +@Component +@TbCoreComponent +public class AssetProfileMsgConstructor { + + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + + public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) { + AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder() + .setMsgType(msgType) + .setIdMSB(assetProfile.getId().getId().getMostSignificantBits()) + .setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()) + .setName(assetProfile.getName()) + .setDefault(assetProfile.isDefault()); + if (assetProfile.getDefaultRuleChainId() != null) { + builder.setDefaultRuleChainIdMSB(assetProfile.getDefaultRuleChainId().getId().getMostSignificantBits()) + .setDefaultRuleChainIdLSB(assetProfile.getDefaultRuleChainId().getId().getLeastSignificantBits()); + } + if (assetProfile.getDefaultQueueName() != null) { + builder.setDefaultQueueName(assetProfile.getDefaultQueueName()); + } + if (assetProfile.getDescription() != null) { + builder.setDescription(assetProfile.getDescription()); + } + if (assetProfile.getImage() != null) { + builder.setImage(ByteString.copyFrom(assetProfile.getImage().getBytes(StandardCharsets.UTF_8))); + } + return builder.build(); + } + + public AssetProfileUpdateMsg constructAssetProfileDeleteMsg(AssetProfileId assetProfileId) { + return AssetProfileUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(assetProfileId.getId().getMostSignificantBits()) + .setIdLSB(assetProfileId.getId().getLeastSignificantBits()).build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java new file mode 100644 index 0000000000..efade1b235 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +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.asset.AssetProfileService; + +@AllArgsConstructor +@Slf4j +public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final AssetProfileService assetProfileService; + + @Override + PageData fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) { + return assetProfileService.findAssetProfiles(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AssetProfile assetProfile) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE, + EdgeEventActionType.ADDED, assetProfile.getId(), null); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java new file mode 100644 index 0000000000..ec11764fea --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Component +@Slf4j +@TbCoreComponent +public class AssetProfileEdgeProcessor extends BaseEdgeProcessor { + + public DownlinkMsg processAssetProfileToEdge(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) { + AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); + DownlinkMsg downlinkMsg = null; + switch (action) { + case ADDED: + case UPDATED: + AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId); + if (assetProfile != null) { + AssetProfileUpdateMsg assetProfileUpdateMsg = + assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetProfileUpdateMsg(assetProfileUpdateMsg) + .build(); + } + break; + case DELETED: + AssetProfileUpdateMsg assetProfileUpdateMsg = + assetProfileMsgConstructor.constructAssetProfileDeleteMsg(assetProfileId); + downlinkMsg = DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAssetProfileUpdateMsg(assetProfileUpdateMsg) + .build(); + break; + } + return downlinkMsg; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 9d6199bdcd..018acb7668 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -37,6 +37,7 @@ 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.alarm.AlarmService; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; @@ -61,6 +62,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor; +import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor; @@ -75,6 +77,7 @@ import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor; import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.state.DeviceStateService; @@ -100,6 +103,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected TbDeviceProfileCache deviceProfileCache; + @Autowired + protected TbAssetProfileCache assetProfileCache; + @Autowired protected DashboardService dashboardService; @@ -124,6 +130,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected DeviceProfileService deviceProfileService; + @Autowired + protected AssetProfileService assetProfileService; + @Autowired protected RelationService relationService; @@ -197,6 +206,9 @@ public abstract class BaseEdgeProcessor { @Autowired protected DeviceProfileMsgConstructor deviceProfileMsgConstructor; + @Autowired + protected AssetProfileMsgConstructor assetProfileMsgConstructor; + @Autowired protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java index ad6446cb95..f3bb328886 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; @@ -44,7 +45,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; @@ -159,23 +159,26 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { } private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + RuleChainId ruleChainId = null; + String queueName = null; if (EntityType.DEVICE.equals(entityId.getEntityType())) { DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); - RuleChainId ruleChainId; - String queueName; - if (deviceProfile == null) { log.warn("[{}] Device profile is null!", entityId); - ruleChainId = null; - queueName = null; } else { ruleChainId = deviceProfile.getDefaultRuleChainId(); queueName = deviceProfile.getDefaultQueueName(); } - return new ImmutablePair<>(queueName, ruleChainId); - } else { - return new ImmutablePair<>(null, null); + } else if (EntityType.ASSET.equals(entityId.getEntityType())) { + AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + if (assetProfile == null) { + log.warn("[{}] Asset profile is null!", entityId); + } else { + ruleChainId = assetProfile.getDefaultRuleChainId(); + queueName = assetProfile.getDefaultQueueName(); + } } + return new ImmutablePair<>(queueName, ruleChainId); } private ListenableFuture processPostTelemetry(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index e66d7d5595..8db5c4f4c7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -33,10 +33,13 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; @@ -57,6 +60,8 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; @@ -64,6 +69,7 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileDevicesRequestMsg; @@ -104,6 +110,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private DeviceService deviceService; + @Autowired + private AssetService assetService; + @Lazy @Autowired private TbEntityViewService entityViewService; @@ -111,6 +120,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private DeviceProfileService deviceProfileService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired private WidgetsBundleService widgetsBundleService; @@ -351,6 +363,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } + @Override + public ListenableFuture processAssetProfileAssetsRequestMsg(TenantId tenantId, Edge edge, AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg) { + log.trace("[{}] processAssetProfileAssetsRequestMsg [{}][{}]", tenantId, edge.getName(), assetProfileAssetsRequestMsg); + if (assetProfileAssetsRequestMsg.getAssetProfileIdMSB() == 0 || assetProfileAssetsRequestMsg.getAssetProfileIdLSB() == 0) { + return Futures.immediateFuture(null); + } + AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileAssetsRequestMsg.getAssetProfileIdMSB(), assetProfileAssetsRequestMsg.getAssetProfileIdLSB())); + AssetProfile assetProfileById = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + if (assetProfileById == null) { + return Futures.immediateFuture(null); + } + return syncAssets(tenantId, edge, assetProfileById.getName()); + } + + private ListenableFuture syncAssets(TenantId tenantId, Edge edge, String assetType) { + log.trace("[{}] syncAssets [{}][{}]", tenantId, edge.getName(), assetType); + List> futures = new ArrayList<>(); + try { + PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); + PageData pageData; + do { + pageData = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edge.getId(), assetType, pageLink); + if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { + log.trace("[{}] [{}] asset(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size()); + for (Asset asset : pageData.getData()) { + futures.add(saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.ADDED, asset.getId(), null)); + } + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } + } while (pageData != null && pageData.hasNext()); + } catch (Exception e) { + log.error("Exception during loading edge asset(s) on sync!", e); + } + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + } + @Override public ListenableFuture processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 2ba1b3c9d3..7140ac52a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc.sync; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileDevicesRequestMsg; @@ -41,6 +42,8 @@ public interface EdgeRequestsService { ListenableFuture processDeviceProfileDevicesRequestMsg(TenantId tenantId, Edge edge, DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg); + ListenableFuture processAssetProfileAssetsRequestMsg(TenantId tenantId, Edge edge, AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg); + ListenableFuture processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg); ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java new file mode 100644 index 0000000000..b964bfcd1a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.asset.profile; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +@Service +@TbCoreComponent +@AllArgsConstructor +@Slf4j +public class DefaultTbAssetProfileService extends AbstractTbEntityService implements TbAssetProfileService { + + private final AssetProfileService assetProfileService; + + @Override + public AssetProfile save(AssetProfile assetProfile, User user) throws Exception { + ActionType actionType = assetProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + TenantId tenantId = assetProfile.getTenantId(); + try { + AssetProfile savedAssetProfile = checkNotNull(assetProfileService.saveAssetProfile(assetProfile)); + autoCommit(user, assetProfile.getId()); + tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAssetProfile.getId(), + actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedAssetProfile.getId(), + savedAssetProfile, user, actionType, true, null); + return savedAssetProfile; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), assetProfile, actionType, user, e); + throw e; + } + } + + @Override + public void delete(AssetProfile assetProfile, User user) { + AssetProfileId assetProfileId = assetProfile.getId(); + TenantId tenantId = assetProfile.getTenantId(); + try { + assetProfileService.deleteAssetProfile(tenantId, assetProfileId); + + tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetProfileId, ComponentLifecycleEvent.DELETED); + notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, assetProfileId, assetProfile, + user, ActionType.DELETED, true, null, assetProfileId.toString()); + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), ActionType.DELETED, + user, e, assetProfileId.toString()); + throw e; + } + } + + @Override + public AssetProfile setDefaultAssetProfile(AssetProfile assetProfile, AssetProfile previousDefaultAssetProfile, User user) throws ThingsboardException { + TenantId tenantId = assetProfile.getTenantId(); + AssetProfileId assetProfileId = assetProfile.getId(); + try { + if (assetProfileService.setDefaultAssetProfile(tenantId, assetProfileId)) { + if (previousDefaultAssetProfile != null) { + previousDefaultAssetProfile = assetProfileService.findAssetProfileById(tenantId, previousDefaultAssetProfile.getId()); + notificationEntityService.logEntityAction(tenantId, previousDefaultAssetProfile.getId(), previousDefaultAssetProfile, + ActionType.UPDATED, user); + } + assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + + notificationEntityService.logEntityAction(tenantId, assetProfileId, assetProfile, ActionType.UPDATED, user); + } + return assetProfile; + } catch (Exception e) { + notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), ActionType.UPDATED, + user, e, assetProfileId.toString()); + throw e; + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java new file mode 100644 index 0000000000..e451b3245a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.entitiy.asset.profile; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.service.entitiy.SimpleTbEntityService; + +public interface TbAssetProfileService extends SimpleTbEntityService { + + AssetProfile setDefaultAssetProfile(AssetProfile assetProfile, AssetProfile previousDefaultAssetProfile, User user) throws ThingsboardException; +} 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 ea976b2dec..c5c81bfc91 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 @@ -39,6 +39,8 @@ import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; @@ -117,9 +119,15 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @Autowired private DeviceService deviceService; + @Autowired + private AssetService assetService; + @Autowired private DeviceProfileService deviceProfileService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired private ApiUsageStateService apiUsageStateService; @@ -609,6 +617,53 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.4.1": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3004001)) { + + try { + conn.createStatement().execute("ALTER TABLE asset ADD COLUMN asset_profile_id uuid"); + } catch (Exception e) { + } + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_before.sql"); + loadSql(schemaUpdateFile, conn); + + log.info("Creating default asset profiles..."); + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = tenantService.findTenants(pageLink); + for (Tenant tenant : pageData.getData()) { + List assetTypes = assetService.findAssetTypesByTenantId(tenant.getId()).get(); + try { + assetProfileService.createDefaultAssetProfile(tenant.getId()); + } catch (Exception e) { + } + for (EntitySubtype assetType : assetTypes) { + try { + assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType.getType()); + } catch (Exception e) { + } + } + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + + log.info("Updating asset profiles..."); + conn.createStatement().execute("call update_asset_profiles()"); + + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_after.sql"); + loadSql(schemaUpdateFile, conn); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004002;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java new file mode 100644 index 0000000000..23d0e1b616 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.asset.AssetService; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +@Service +@Slf4j +public class DefaultTbAssetProfileCache implements TbAssetProfileCache { + + private final Lock assetProfileFetchLock = new ReentrantLock(); + private final AssetProfileService assetProfileService; + private final AssetService assetService; + + private final ConcurrentMap assetProfilesMap = new ConcurrentHashMap<>(); + private final ConcurrentMap assetsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap>> profileListeners = new ConcurrentHashMap<>(); + private final ConcurrentMap>> assetProfileListeners = new ConcurrentHashMap<>(); + + public DefaultTbAssetProfileCache(AssetProfileService assetProfileService, AssetService assetService) { + this.assetProfileService = assetProfileService; + this.assetService = assetService; + } + + @Override + public AssetProfile get(TenantId tenantId, AssetProfileId assetProfileId) { + AssetProfile profile = assetProfilesMap.get(assetProfileId); + if (profile == null) { + assetProfileFetchLock.lock(); + try { + profile = assetProfilesMap.get(assetProfileId); + if (profile == null) { + profile = assetProfileService.findAssetProfileById(tenantId, assetProfileId); + if (profile != null) { + assetProfilesMap.put(assetProfileId, profile); + log.debug("[{}] Fetch asset profile into cache: {}", profile.getId(), profile); + } + } + } finally { + assetProfileFetchLock.unlock(); + } + } + log.trace("[{}] Found asset profile in cache: {}", assetProfileId, profile); + return profile; + } + + @Override + public AssetProfile get(TenantId tenantId, AssetId assetId) { + AssetProfileId profileId = assetsMap.get(assetId); + if (profileId == null) { + Asset asset = assetService.findAssetById(tenantId, assetId); + if (asset != null) { + profileId = asset.getAssetProfileId(); + assetsMap.put(assetId, profileId); + } else { + return null; + } + } + return get(tenantId, profileId); + } + + @Override + public void evict(TenantId tenantId, AssetProfileId profileId) { + AssetProfile oldProfile = assetProfilesMap.remove(profileId); + log.debug("[{}] evict asset profile from cache: {}", profileId, oldProfile); + AssetProfile newProfile = get(tenantId, profileId); + if (newProfile != null) { + notifyProfileListeners(newProfile); + } + } + + @Override + public void evict(TenantId tenantId, AssetId assetId) { + AssetProfileId old = assetsMap.remove(assetId); + if (old != null) { + AssetProfile newProfile = get(tenantId, assetId); + if (newProfile == null || !old.equals(newProfile.getId())) { + notifyAssetListeners(tenantId, assetId, newProfile); + } + } + } + + @Override + public void addListener(TenantId tenantId, EntityId listenerId, + Consumer profileListener, + BiConsumer assetListener) { + if (profileListener != null) { + profileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, profileListener); + } + if (assetListener != null) { + assetProfileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, assetListener); + } + } + + @Override + public AssetProfile find(AssetProfileId assetProfileId) { + return assetProfileService.findAssetProfileById(TenantId.SYS_TENANT_ID, assetProfileId); + } + + @Override + public AssetProfile findOrCreateAssetProfile(TenantId tenantId, String profileName) { + return assetProfileService.findOrCreateAssetProfile(tenantId, profileName); + } + + @Override + public void removeListener(TenantId tenantId, EntityId listenerId) { + ConcurrentMap> tenantListeners = profileListeners.get(tenantId); + if (tenantListeners != null) { + tenantListeners.remove(listenerId); + } + ConcurrentMap> assetListeners = assetProfileListeners.get(tenantId); + if (assetListeners != null) { + assetListeners.remove(listenerId); + } + } + + private void notifyProfileListeners(AssetProfile profile) { + ConcurrentMap> tenantListeners = profileListeners.get(profile.getTenantId()); + if (tenantListeners != null) { + tenantListeners.forEach((id, listener) -> listener.accept(profile)); + } + } + + private void notifyAssetListeners(TenantId tenantId, AssetId assetId, AssetProfile profile) { + if (profile != null) { + ConcurrentMap> tenantListeners = assetProfileListeners.get(tenantId); + if (tenantListeners != null) { + tenantListeners.forEach((id, listener) -> listener.accept(assetId, profile)); + } + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java new file mode 100644 index 0000000000..6c332e63ff --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.profile; + +import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbAssetProfileCache extends RuleEngineAssetProfileCache { + + void evict(TenantId tenantId, AssetProfileId id); + + void evict(TenantId tenantId, AssetId id); + + AssetProfile find(AssetProfileId assetProfileId); + + AssetProfile findOrCreateAssetProfile(TenantId tenantId, String assetType); +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 8ec29672de..ca654ec802 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -35,8 +35,11 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; @@ -69,6 +72,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Set; @@ -108,6 +112,7 @@ public class DefaultTbClusterService implements TbClusterService { private final NotificationsTopicService notificationsTopicService; private final DataDecodingEncodingService encodingService; private final TbDeviceProfileCache deviceProfileCache; + private final TbAssetProfileCache assetProfileCache; private final GatewayNotificationsService gatewayNotificationsService; @Override @@ -177,6 +182,10 @@ public class DefaultTbClusterService implements TbClusterService { tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()))); } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); + } else if (entityId.getEntityType().equals(EntityType.ASSET)) { + tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetId(entityId.getId()))); + } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { + tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId()))); } } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); @@ -193,16 +202,30 @@ public class DefaultTbClusterService implements TbClusterService { if (deviceProfile != null) { RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); String targetQueueName = deviceProfile.getDefaultQueueName(); - boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); - boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); - - if (isRuleChainTransform && isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); - } else if (isRuleChainTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); - } else if (isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); - } + tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } + return tbMsg; + } + + private TbMsg transformMsg(TbMsg tbMsg, AssetProfile assetProfile) { + if (assetProfile != null) { + RuleChainId targetRuleChainId = assetProfile.getDefaultRuleChainId(); + String targetQueueName = assetProfile.getDefaultQueueName(); + tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } + return tbMsg; + } + + private TbMsg transformMsg(TbMsg tbMsg, RuleChainId targetRuleChainId, String targetQueueName) { + boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); + boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); + + if (isRuleChainTransform && isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } else if (isRuleChainTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); + } else if (isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); } return tbMsg; } @@ -367,6 +390,7 @@ public class DefaultTbClusterService implements TbClusterService { if (entityType.equals(EntityType.TENANT) || entityType.equals(EntityType.TENANT_PROFILE) || entityType.equals(EntityType.DEVICE_PROFILE) + || entityType.equals(EntityType.ASSET_PROFILE) || entityType.equals(EntityType.API_USAGE_STATE) || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index a3bae8bc4e..9cdf3e4aa2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -67,6 +67,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.edge.EdgeNotificationService; import org.thingsboard.server.service.ota.OtaPackageStateService; +import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; @@ -138,6 +139,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> nfConsumer) { + TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService, + PartitionService partitionService, TbQueueConsumer> nfConsumer) { this.actorContext = actorContext; this.encodingService = encodingService; this.tenantProfileCache = tenantProfileCache; this.deviceProfileCache = deviceProfileCache; + this.assetProfileCache = assetProfileCache; this.apiUsageStateService = apiUsageStateService; this.partitionService = partitionService; this.nfConsumer = nfConsumer; @@ -180,6 +183,10 @@ public abstract class AbstractConsumerService callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + AssetProfile assetProfile = assetProfileService.findAssetProfileById(currentUser.getTenantId(), new AssetProfileId(entityId.getId())); + if (assetProfile == null) { + callback.onSuccess(ValidationResult.entityNotFound("Asset profile with requested id wasn't found!")); + } else { + try { + accessControlService.checkPermission(currentUser, Resource.ASSET_PROFILE, operation, entityId, assetProfile); + } catch (ThingsboardException e) { + callback.onSuccess(ValidationResult.accessDenied(e.getMessage())); + } + callback.onSuccess(ValidationResult.ok(assetProfile)); + } + } + } + private void validateApiUsageState(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index ff57aec801..cdd84b0478 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -42,7 +42,8 @@ public class CustomerUserPermissions extends AbstractPermissions { put(Resource.WIDGET_TYPE, widgetsPermissionChecker); put(Resource.EDGE, customerEntityPermissionChecker); put(Resource.RPC, rpcPermissionChecker); - put(Resource.DEVICE_PROFILE, deviceProfilePermissionChecker); + put(Resource.DEVICE_PROFILE, profilePermissionChecker); + put(Resource.ASSET_PROFILE, profilePermissionChecker); } private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() { @@ -154,7 +155,7 @@ public class CustomerUserPermissions extends AbstractPermissions { } }; - private static final PermissionChecker deviceProfilePermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) { + private static final PermissionChecker profilePermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) { @Override @SuppressWarnings("unchecked") 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 3b1e4f5fa2..a9484a2bd3 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 @@ -36,6 +36,7 @@ public enum Resource { OAUTH2_CONFIGURATION_TEMPLATE(), TENANT_PROFILE(EntityType.TENANT_PROFILE), DEVICE_PROFILE(EntityType.DEVICE_PROFILE), + ASSET_PROFILE(EntityType.ASSET_PROFILE), API_USAGE_STATE(EntityType.API_USAGE_STATE), TB_RESOURCE(EntityType.TB_RESOURCE), OTA_PACKAGE(EntityType.OTA_PACKAGE), 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 fe61f16ad3..ef7991e5de 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 @@ -41,6 +41,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker); put(Resource.WIDGET_TYPE, widgetsPermissionChecker); put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker); + put(Resource.ASSET_PROFILE, tenantEntityPermissionChecker); put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker); put(Resource.TB_RESOURCE, tbResourcePermissionChecker); put(Resource.OTA_PACKAGE, tenantEntityPermissionChecker); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 0f43159e30..f4e4e3dc14 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -64,7 +64,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final TbNotificationEntityService entityNotificationService; protected static final List SUPPORTED_ENTITY_TYPES = List.of( - EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN, + EntityType.CUSTOMER, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.RULE_CHAIN, EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java index 6e06144ae2..b5f5e6afd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -183,7 +185,7 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi @Autowired private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService, DashboardService dashboardService, DeviceProfileService deviceProfileService, - DeviceService deviceService, WidgetsBundleService widgetsBundleService) { + AssetProfileService assetProfileService, DeviceService deviceService, WidgetsBundleService widgetsBundleService) { removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> { customerService.deleteCustomer(tenantId, (CustomerId) entityId); }); @@ -199,6 +201,9 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> { deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId); }); + removers.put(EntityType.ASSET_PROFILE, (tenantId, entityId) -> { + assetProfileService.deleteAssetProfile(tenantId, (AssetProfileId) entityId); + }); removers.put(EntityType.DEVICE, (tenantId, entityId) -> { deviceService.deleteDevice(tenantId, (DeviceId) entityId); }); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java index 8875491d42..5ae496558e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java @@ -32,6 +32,7 @@ public class AssetExportService extends BaseEntityExportService ctx, Asset asset, EntityExportData exportData) { asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId())); + asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId())); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java new file mode 100644 index 0000000000..a333767b33 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; + +import java.util.Set; + +@Service +@TbCoreComponent +public class AssetProfileExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, AssetProfile assetProfile, EntityExportData exportData) { + assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId())); + assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId())); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.ASSET_PROFILE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 3225668004..fa3558520e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -41,6 +41,7 @@ public class AssetImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId())); return asset; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java new file mode 100644 index 0000000000..7e8a53a7b0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.ota.OtaPackageStateService; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +public class AssetProfileImportService extends BaseEntityImportService> { + + private final AssetProfileService assetProfileService; + private final OtaPackageStateService otaPackageStateService; + + @Override + protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) { + assetProfile.setTenantId(tenantId); + } + + @Override + protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData exportData, IdProvider idProvider) { + assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId())); + assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId())); + return assetProfile; + } + + @Override + protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData exportData, IdProvider idProvider) { + return assetProfileService.saveAssetProfile(assetProfile); + } + + @Override + protected void onEntitySaved(User user, AssetProfile savedAssetProfile, AssetProfile oldAssetProfile) throws ThingsboardException { + clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedAssetProfile.getId(), + oldAssetProfile == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + entityNotificationService.notifyCreateOrUpdateOrDelete(savedAssetProfile.getTenantId(), null, + savedAssetProfile.getId(), savedAssetProfile, user, oldAssetProfile == null ? ActionType.ADDED : ActionType.UPDATED, true, null); + } + + @Override + protected AssetProfile deepCopy(AssetProfile assetProfile) { + return new AssetProfile(assetProfile); + } + + @Override + protected void cleanupForComparison(AssetProfile assetProfile) { + super.cleanupForComparison(assetProfile); + } + + @Override + public EntityType getEntityType() { + return EntityType.ASSET_PROFILE; + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9edd5d0ed5..7b7dc2bba8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -406,6 +406,9 @@ cache: deviceProfiles: timeToLiveInMinutes: "${CACHE_SPECS_DEVICE_PROFILES_TTL:1440}" maxSize: "${CACHE_SPECS_DEVICE_PROFILES_MAX_SIZE:10000}" + assetProfiles: + timeToLiveInMinutes: "${CACHE_SPECS_ASSET_PROFILES_TTL:1440}" + maxSize: "${CACHE_SPECS_ASSET_PROFILES_MAX_SIZE:10000}" attributes: timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}" maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}" @@ -561,6 +564,7 @@ audit-log: "alarm": "${AUDIT_LOG_MASK_ALARM:W}" "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" + "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" "edge": "${AUDIT_LOG_MASK_EDGE:W}" "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 09c40f263c..ae3fc6d16d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -59,6 +59,7 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; @@ -448,6 +449,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return deviceProfile; } + protected AssetProfile createAssetProfile(String name) { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setName(name); + assetProfile.setDescription(name + " Test"); + assetProfile.setDefault(false); + assetProfile.setDefaultRuleChainId(null); + return assetProfile; + } + protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) { MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration(); mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 4e9c01e4cc..65b1687039 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetId; @@ -187,6 +188,20 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { deleteDifferentTenant(); } + @Test + public void testSaveAssetWithProfileFromDifferentTenant() throws Exception { + loginDifferentTenant(); + AssetProfile differentProfile = createAssetProfile("Different profile"); + differentProfile = doPost("/api/assetProfile", differentProfile, AssetProfile.class); + + loginTenantAdmin(); + Asset asset = new Asset(); + asset.setName("My device"); + asset.setAssetProfileId(differentProfile.getId()); + doPost("/api/asset", asset).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Asset can`t be referencing to asset profile from different tenant!"))); + } + @Test public void testFindAssetById() throws Exception { Asset asset = new Asset(); @@ -310,13 +325,12 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - String msgError = "Asset type " + msgErrorShouldBeSpecified; - doPost("/api/asset", asset) - .andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString(msgError))); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + Assert.assertEquals("default", savedAsset.getType()); - testNotifyEntityEqualsOneTimeServiceNeverError(asset, savedTenant.getId(), - tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + testNotifyEntityOneTimeMsgToEdgeServiceNever(savedAsset, savedAsset.getId(), savedAsset.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java new file mode 100644 index 0000000000..01b763a4b8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java @@ -0,0 +1,452 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.asset.AssetProfileDao; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseAssetProfileControllerTest extends AbstractControllerTest { + + private IdComparator idComparator = new IdComparator<>(); + private IdComparator assetProfileInfoIdComparator = new IdComparator<>(); + + private Tenant savedTenant; + private User tenantAdmin; + + @SpyBean + private AssetProfileDao assetProfileDao; + + @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(); + + afterTestEntityDaoRemoveByIdWithException (assetProfileDao); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveAssetProfile() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + + Mockito.reset(tbClusterService, auditLogService); + + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + Assert.assertNotNull(savedAssetProfile); + Assert.assertNotNull(savedAssetProfile.getId()); + Assert.assertTrue(savedAssetProfile.getCreatedTime() > 0); + Assert.assertEquals(assetProfile.getName(), savedAssetProfile.getName()); + Assert.assertEquals(assetProfile.getDescription(), savedAssetProfile.getDescription()); + Assert.assertEquals(assetProfile.isDefault(), savedAssetProfile.isDefault()); + Assert.assertEquals(assetProfile.getDefaultRuleChainId(), savedAssetProfile.getDefaultRuleChainId()); + + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedAssetProfile, savedAssetProfile.getId(), savedAssetProfile.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED); + + savedAssetProfile.setName("New asset profile"); + doPost("/api/assetProfile", savedAssetProfile, AssetProfile.class); + AssetProfile foundAssetProfile = doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString(), AssetProfile.class); + Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfile.getName()); + + testNotifyEntityBroadcastEntityStateChangeEventOneTime(foundAssetProfile, foundAssetProfile.getId(), foundAssetProfile.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.UPDATED); + } + + @Test + public void saveAssetProfileWithViolationOfValidation() throws Exception { + String msgError = msgErrorFieldLength("name"); + + Mockito.reset(tbClusterService, auditLogService); + + AssetProfile createAssetProfile = this.createAssetProfile(StringUtils.randomAlphabetic(300)); + doPost("/api/assetProfile", createAssetProfile) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(msgError))); + + testNotifyEntityEqualsOneTimeServiceNeverError(createAssetProfile, savedTenant.getId(), + tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + } + + @Test + public void testFindAssetProfileById() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + AssetProfile foundAssetProfile = doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString(), AssetProfile.class); + Assert.assertNotNull(foundAssetProfile); + Assert.assertEquals(savedAssetProfile, foundAssetProfile); + } + + @Test + public void whenGetAssetProfileById_thenPermissionsAreChecked() throws Exception { + AssetProfile assetProfile = createAssetProfile("Asset profile 1"); + assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + loginDifferentTenant(); + + doGet("/api/assetProfile/" + assetProfile.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + } + + @Test + public void testFindAssetProfileInfoById() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + AssetProfileInfo foundAssetProfileInfo = doGet("/api/assetProfileInfo/" + savedAssetProfile.getId().getId().toString(), AssetProfileInfo.class); + Assert.assertNotNull(foundAssetProfileInfo); + Assert.assertEquals(savedAssetProfile.getId(), foundAssetProfileInfo.getId()); + Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName()); + + Customer customer = new Customer(); + customer.setTitle("Customer"); + customer.setTenantId(savedTenant.getId()); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + User customerUser = new User(); + customerUser.setAuthority(Authority.CUSTOMER_USER); + customerUser.setTenantId(savedTenant.getId()); + customerUser.setCustomerId(savedCustomer.getId()); + customerUser.setEmail("customer2@thingsboard.org"); + + createUserAndLogin(customerUser, "customer"); + + foundAssetProfileInfo = doGet("/api/assetProfileInfo/" + savedAssetProfile.getId().getId().toString(), AssetProfileInfo.class); + Assert.assertNotNull(foundAssetProfileInfo); + Assert.assertEquals(savedAssetProfile.getId(), foundAssetProfileInfo.getId()); + Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName()); + } + + @Test + public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception { + AssetProfile assetProfile = createAssetProfile("Asset profile 1"); + assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + loginDifferentTenant(); + doGet("/api/assetProfileInfo/" + assetProfile.getId()) + .andExpect(status().isForbidden()) + .andExpect(statusReason(containsString(msgErrorPermission))); + } + + @Test + public void testFindDefaultAssetProfileInfo() throws Exception { + AssetProfileInfo foundDefaultAssetProfileInfo = doGet("/api/assetProfileInfo/default", AssetProfileInfo.class); + Assert.assertNotNull(foundDefaultAssetProfileInfo); + Assert.assertNotNull(foundDefaultAssetProfileInfo.getId()); + Assert.assertNotNull(foundDefaultAssetProfileInfo.getName()); + Assert.assertEquals("default", foundDefaultAssetProfileInfo.getName()); + } + + @Test + public void testSetDefaultAssetProfile() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile 1"); + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + Mockito.reset(tbClusterService, auditLogService); + + AssetProfile defaultAssetProfile = doPost("/api/assetProfile/" + savedAssetProfile.getId().getId().toString() + "/default", AssetProfile.class); + Assert.assertNotNull(defaultAssetProfile); + AssetProfileInfo foundDefaultAssetProfile = doGet("/api/assetProfileInfo/default", AssetProfileInfo.class); + Assert.assertNotNull(foundDefaultAssetProfile); + Assert.assertEquals(savedAssetProfile.getName(), foundDefaultAssetProfile.getName()); + Assert.assertEquals(savedAssetProfile.getId(), foundDefaultAssetProfile.getId()); + + testNotifyEntityOneTimeMsgToEdgeServiceNever(defaultAssetProfile, defaultAssetProfile.getId(), defaultAssetProfile.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.UPDATED); + } + + @Test + public void testSaveAssetProfileWithEmptyName() throws Exception { + AssetProfile assetProfile = new AssetProfile(); + + Mockito.reset(tbClusterService, auditLogService); + + String msgError = "Asset profile name " + msgErrorShouldBeSpecified; + doPost("/api/assetProfile", assetProfile) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(msgError))); + + testNotifyEntityEqualsOneTimeServiceNeverError(assetProfile, savedTenant.getId(), + tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + } + + @Test + public void testSaveAssetProfileWithSameName() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + doPost("/api/assetProfile", assetProfile).andExpect(status().isOk()); + AssetProfile assetProfile2 = this.createAssetProfile("Asset Profile"); + + Mockito.reset(tbClusterService, auditLogService); + + String msgError = "Asset profile with such name already exists"; + doPost("/api/assetProfile", assetProfile2) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString(msgError))); + + testNotifyEntityEqualsOneTimeServiceNeverError(assetProfile, savedTenant.getId(), + tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError)); + } + + @Test + public void testDeleteAssetProfileWithExistingAsset() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + Asset asset = new Asset(); + asset.setName("Test asset"); + asset.setAssetProfileId(savedAssetProfile.getId()); + + doPost("/api/asset", asset, Asset.class); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/assetProfile/" + savedAssetProfile.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("The asset profile referenced by the assets cannot be deleted"))); + + testNotifyEntityNever(savedAssetProfile.getId(), savedAssetProfile); + } + + @Test + public void testSaveAssetProfileWithRuleChainFromDifferentTenant() throws Exception { + loginDifferentTenant(); + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("Different rule chain"); + RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + + loginTenantAdmin(); + + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + assetProfile.setDefaultRuleChainId(savedRuleChain.getId()); + doPost("/api/assetProfile", assetProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't assign rule chain from different tenant!"))); + } + + @Test + public void testSaveAssetProfileWithDashboardFromDifferentTenant() throws Exception { + loginDifferentTenant(); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("Different dashboard"); + Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); + + loginTenantAdmin(); + + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + assetProfile.setDefaultDashboardId(savedDashboard.getId()); + doPost("/api/assetProfile", assetProfile).andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Can't assign dashboard from different tenant!"))); + } + + @Test + public void testDeleteAssetProfile() throws Exception { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile"); + AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class); + + Mockito.reset(tbClusterService, auditLogService); + + doDelete("/api/assetProfile/" + savedAssetProfile.getId().getId().toString()) + .andExpect(status().isOk()); + + String savedAssetProfileIdStr = savedAssetProfile.getId().getId().toString(); + testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedAssetProfile, savedAssetProfile.getId(), savedAssetProfile.getId(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.DELETED, savedAssetProfileIdStr); + + doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString()) + .andExpect(status().isNotFound()) + .andExpect(statusReason(containsString(msgErrorNoFound("Asset profile", savedAssetProfileIdStr)))); + } + + @Test + public void testFindAssetProfiles() throws Exception { + List assetProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = doGetTypedWithPageLink("/api/assetProfiles?", + new TypeReference<>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + assetProfiles.addAll(pageData.getData()); + + Mockito.reset(tbClusterService, auditLogService); + + int cntEntity = 28; + for (int i = 0; i < cntEntity; i++) { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i); + assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfile.class)); + } + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new AssetProfile(), new AssetProfile(), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity); + Mockito.reset(tbClusterService, auditLogService); + + List loadedAssetProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = doGetTypedWithPageLink("/api/assetProfiles?", + new TypeReference<>() { + }, pageLink); + loadedAssetProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(assetProfiles, idComparator); + Collections.sort(loadedAssetProfiles, idComparator); + + Assert.assertEquals(assetProfiles, loadedAssetProfiles); + + for (AssetProfile assetProfile : loadedAssetProfiles) { + if (!assetProfile.isDefault()) { + doDelete("/api/assetProfile/" + assetProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(loadedAssetProfiles.get(0), loadedAssetProfiles.get(0), + savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), + ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedAssetProfiles.get(0).getId().getId().toString()); + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/assetProfiles?", + new TypeReference<>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindAssetProfileInfos() throws Exception { + List assetProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData assetProfilePageData = doGetTypedWithPageLink("/api/assetProfiles?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(assetProfilePageData.hasNext()); + Assert.assertEquals(1, assetProfilePageData.getTotalElements()); + assetProfiles.addAll(assetProfilePageData.getData()); + + for (int i = 0; i < 28; i++) { + AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i); + assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfile.class)); + } + + List loadedAssetProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = doGetTypedWithPageLink("/api/assetProfileInfos?", + new TypeReference<>() { + }, pageLink); + loadedAssetProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(assetProfiles, idComparator); + Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator); + + List assetProfileInfos = assetProfiles.stream().map(assetProfile -> new AssetProfileInfo(assetProfile.getId(), + assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList()); + + Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos); + + for (AssetProfile assetProfile : assetProfiles) { + if (!assetProfile.isDefault()) { + doDelete("/api/assetProfile/" + assetProfile.getId().getId().toString()) + .andExpect(status().isOk()); + } + } + + pageLink = new PageLink(17); + pageData = doGetTypedWithPageLink("/api/assetProfileInfos?", + new TypeReference>() { + }, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testDeleteAssetProfileWithDeleteRelationsOk() throws Exception { + AssetProfileId assetProfileId = savedAssetProfile("AssetProfile for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), assetProfileId, "/api/assetProfile/" + assetProfileId); + } + + @Test + public void testDeleteAssetProfileExceptionWithRelationsTransactional() throws Exception { + AssetProfileId assetProfileId = savedAssetProfile("AssetProfile for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(assetProfileDao, savedTenant.getId(), assetProfileId, "/api/assetProfile/" + assetProfileId); + } + + private AssetProfile savedAssetProfile(String name) { + AssetProfile assetProfile = createAssetProfile(name); + return doPost("/api/assetProfile", assetProfile, AssetProfile.class); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index c3d747cb35..9582d6cbcf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.edge.imitator.EdgeImitator; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; @@ -808,7 +809,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); - edgeImitator.expectMessageAmount(12); + edgeImitator.expectMessageAmount(14); edgeImitator.connect(); assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue(); @@ -816,17 +817,19 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("one msg during sync process, another from edge creation").hasSize(2); assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("one msg during sync process for 'default' device profile").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("one msg once device assigned to edge").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("two msgs during sync process for 'default' and 'test' asset profiles").hasSize(2); assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2); assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4); - edgeImitator.expectMessageAmount(9); + edgeImitator.expectMessageAmount(11); doPost("/api/edge/sync/" + edge.getId()); assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue(); assertThat(edgeImitator.findAllMessagesByType(QueueUpdateMsg.class)).as("queue msg").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("rule chain msg").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("device profile msg").hasSize(1); + assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("asset profile msg").hasSize(2); assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("asset update msg").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1); assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4); diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java new file mode 100644 index 0000000000..9913369db6 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.sql; + +import org.thingsboard.server.controller.BaseAssetProfileControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AssetProfileControllerSqlTest extends BaseAssetProfileControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java index dcd354ace5..e2a3b6e2d1 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -228,7 +228,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(15); + edgeImitator.expectMessageAmount(17); edgeImitator.connect(); requestEdgeRuleChainMetadata(); diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 1c0436d329..aacea38b12 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -28,6 +28,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; @@ -196,6 +197,11 @@ public class EdgeImitator { result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg)); } } + if (downlinkMsg.getAssetProfileUpdateMsgCount() > 0) { + for (AssetProfileUpdateMsg assetProfileUpdateMsg : downlinkMsg.getAssetProfileUpdateMsgList()) { + result.add(saveDownlinkMsg(assetProfileUpdateMsg)); + } + } if (downlinkMsg.getAssetUpdateMsgCount() > 0) { for (AssetUpdateMsg assetUpdateMsg : downlinkMsg.getAssetUpdateMsgList()) { result.add(saveDownlinkMsg(assetUpdateMsg)); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java index e516d5e131..48eb0c9e27 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java @@ -37,12 +37,14 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -64,6 +66,7 @@ import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportSettings; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -97,6 +100,8 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest @Autowired protected DeviceProfileService deviceProfileService; @Autowired + protected AssetProfileService assetProfileService; + @Autowired protected AssetService assetService; @Autowired protected CustomerService customerService; @@ -206,11 +211,26 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription()); } - protected Asset createAsset(TenantId tenantId, CustomerId customerId, String type, String name) { + protected AssetProfile createAssetProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfile.setName(name); + assetProfile.setDescription("dscrptn"); + assetProfile.setDefaultRuleChainId(defaultRuleChainId); + assetProfile.setDefaultDashboardId(defaultDashboardId); + return assetProfileService.saveAssetProfile(assetProfile); + } + + protected void checkImportedAssetProfileData(AssetProfile initialProfile, AssetProfile importedProfile) { + assertThat(initialProfile.getName()).isEqualTo(importedProfile.getName()); + assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription()); + } + + protected Asset createAsset(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, String name) { Asset asset = new Asset(); asset.setTenantId(tenantId); asset.setCustomerId(customerId); - asset.setType(type); + asset.setAssetProfileId(assetProfileId); asset.setName(name); asset.setLabel("lbl"); asset.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 8cf57e8e37..cc27763435 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -32,12 +32,13 @@ 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.EntityView; -import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -60,7 +61,6 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportSettings; import org.thingsboard.server.common.data.sync.ie.RuleChainExportData; import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.ota.OtaPackageStateService; @@ -77,7 +77,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.verify; @DaoSqlTest @@ -91,18 +90,30 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { private OtaPackageStateService otaPackageStateService; @Test - public void testExportImportAsset_betweenTenants() throws Exception { - Asset asset = createAsset(tenantId1, null, "AB", "Asset of tenant 1"); - EntityExportData exportData = exportEntity(tenantAdmin1, asset.getId()); + public void testExportImportAssetWithProfile_betweenTenants() throws Exception { + AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile of tenant 1"); + Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset of tenant 1"); - EntityImportResult importResult = importEntity(tenantAdmin2, exportData); - checkImportedEntity(tenantId1, asset, tenantId2, importResult.getSavedEntity()); - checkImportedAssetData(asset, importResult.getSavedEntity()); + EntityExportData profileExportData = exportEntity(tenantAdmin1, assetProfile.getId()); + + EntityExportData assetExportData = exportEntity(tenantAdmin1, asset.getId()); + + EntityImportResult profileImportResult = importEntity(tenantAdmin2, profileExportData); + checkImportedEntity(tenantId1, assetProfile, tenantId2, profileImportResult.getSavedEntity()); + checkImportedAssetProfileData(assetProfile, profileImportResult.getSavedEntity()); + + EntityImportResult assetImportResult = importEntity(tenantAdmin2, assetExportData); + Asset importedAsset = assetImportResult.getSavedEntity(); + checkImportedEntity(tenantId1, asset, tenantId2, importedAsset); + checkImportedAssetData(asset, importedAsset); + + assertThat(importedAsset.getAssetProfileId()).isEqualTo(profileImportResult.getSavedEntity().getId()); } @Test public void testExportImportAsset_sameTenant() throws Exception { - Asset asset = createAsset(tenantId1, null, "AB", "Asset v1.0"); + AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0"); + Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset v1.0"); EntityExportData exportData = exportEntity(tenantAdmin1, asset.getId()); EntityImportResult importResult = importEntity(tenantAdmin1, exportData); @@ -112,8 +123,9 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExportImportAsset_sameTenant_withCustomer() throws Exception { + AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0"); Customer customer = createCustomer(tenantId1, "My customer"); - Asset asset = createAsset(tenantId1, customer.getId(), "AB", "My asset"); + Asset asset = createAsset(tenantId1, customer.getId(), assetProfile.getId(), "My asset"); Asset importedAsset = importEntity(tenantAdmin1, this.exportEntity(tenantAdmin1, asset.getId())).getSavedEntity(); assertThat(importedAsset.getCustomerId()).isEqualTo(asset.getCustomerId()); @@ -238,8 +250,9 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExportImportDashboard_betweenTenants_withEntityAliases() throws Exception { - Asset asset1 = createAsset(tenantId1, null, "A", "Asset 1"); - Asset asset2 = createAsset(tenantId1, null, "A", "Asset 2"); + AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "A"); + Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); + Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); String entityAliases = "{\n" + @@ -263,10 +276,13 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { dashboard.setConfiguration(dashboardConfiguration); dashboard = dashboardService.saveDashboard(dashboard); + EntityExportData profileExportData = exportEntity(tenantAdmin1, assetProfile.getId()); + EntityExportData asset1ExportData = exportEntity(tenantAdmin1, asset1.getId()); EntityExportData asset2ExportData = exportEntity(tenantAdmin1, asset2.getId()); EntityExportData dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId()); + AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity(); Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity(); Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); @@ -310,7 +326,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExportImportWithInboundRelations_betweenTenants() throws Exception { - Asset asset = createAsset(tenantId1, null, "A", "Asset 1"); + Asset asset = createAsset(tenantId1, null, null, "Asset 1"); Device device = createDevice(tenantId1, null, null, "Device 1"); EntityRelation relation = createRelation(asset.getId(), device.getId()); @@ -324,6 +340,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { assertThat(deviceExportData.getRelations().get(0)).matches(entityRelation -> { return entityRelation.getFrom().equals(asset.getId()) && entityRelation.getTo().equals(device.getId()); }); + ((Asset) assetExportData.getEntity()).setAssetProfileId(null); ((Device) deviceExportData.getEntity()).setDeviceProfileId(null); Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity(); @@ -344,7 +361,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExportImportWithRelations_betweenTenants() throws Exception { - Asset asset = createAsset(tenantId1, null, "A", "Asset 1"); + Asset asset = createAsset(tenantId1, null, null, "Asset 1"); Device device = createDevice(tenantId1, null, null, "Device 1"); EntityRelation relation = createRelation(asset.getId(), device.getId()); @@ -353,6 +370,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { .exportRelations(true) .exportCredentials(false) .build()); + assetExportData.getEntity().setAssetProfileId(null); deviceExportData.getEntity().setDeviceProfileId(null); Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity(); @@ -371,7 +389,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExportImportWithRelations_sameTenant() throws Exception { - Asset asset = createAsset(tenantId1, null, "A", "Asset 1"); + Asset asset = createAsset(tenantId1, null, null, "Asset 1"); Device device1 = createDevice(tenantId1, null, null, "Device 1"); EntityRelation relation1 = createRelation(asset.getId(), device1.getId()); @@ -394,7 +412,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void textExportImportWithRelations_sameTenant_removeExisting() throws Exception { - Asset asset1 = createAsset(tenantId1, null, "A", "Asset 1"); + Asset asset1 = createAsset(tenantId1, null, null, "Asset 1"); Device device = createDevice(tenantId1, null, null, "Device 1"); EntityRelation relation1 = createRelation(asset1.getId(), device.getId()); @@ -403,7 +421,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { .build()); assertThat(deviceExportData.getRelations()).size().isOne(); - Asset asset2 = createAsset(tenantId1, null, "A", "Asset 2"); + Asset asset2 = createAsset(tenantId1, null, null, "Asset 2"); EntityRelation relation2 = createRelation(asset2.getId(), device.getId()); importEntity(tenantAdmin1, deviceExportData, EntityImportSettings.builder() @@ -443,14 +461,15 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testEntityEventsOnImport() throws Exception { Customer customer = createCustomer(tenantId1, "Customer 1"); - Asset asset = createAsset(tenantId1, null, "A", "Asset 1"); RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); + AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1"); + Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() @@ -480,6 +499,21 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { Mockito.reset(entityActionService); + RuleChain importedRuleChain = (RuleChain) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.RULE_CHAIN)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedRuleChain.getId()), eq(importedRuleChain), + any(), eq(ActionType.ADDED), isNull()); + verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedRuleChain.getId()), eq(ComponentLifecycleEvent.CREATED)); + + Dashboard importedDashboard = (Dashboard) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DASHBOARD)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedDashboard.getId()), eq(importedDashboard), + any(), eq(ActionType.ADDED), isNull()); + + AssetProfile importedAssetProfile = (AssetProfile) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.ASSET_PROFILE)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedAssetProfile.getId()), eq(importedAssetProfile), + any(), eq(ActionType.ADDED), isNull()); + verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedAssetProfile.getId()), eq(ComponentLifecycleEvent.CREATED)); + verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedAssetProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED)); + Asset importedAsset = (Asset) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.ASSET)).getSavedEntity(); verify(entityActionService).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset), any(), eq(ActionType.ADDED), isNull()); @@ -496,15 +530,6 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { any(), eq(ActionType.UPDATED), isNull()); verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedAsset.getId()), any(), any(), eq(EdgeEventActionType.UPDATED)); - RuleChain importedRuleChain = (RuleChain) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.RULE_CHAIN)).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(importedRuleChain.getId()), eq(importedRuleChain), - any(), eq(ActionType.ADDED), isNull()); - verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedRuleChain.getId()), eq(ComponentLifecycleEvent.CREATED)); - - Dashboard importedDashboard = (Dashboard) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DASHBOARD)).getSavedEntity(); - verify(entityActionService).logEntityAction(any(), eq(importedDashboard.getId()), eq(importedDashboard), - any(), eq(ActionType.ADDED), isNull()); - DeviceProfile importedDeviceProfile = (DeviceProfile) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE_PROFILE)).getSavedEntity(); verify(entityActionService).logEntityAction(any(), eq(importedDeviceProfile.getId()), eq(importedDeviceProfile), any(), eq(ActionType.ADDED), isNull()); @@ -529,15 +554,21 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { @Test public void testExternalIdsInExportData() throws Exception { Customer customer = createCustomer(tenantId1, "Customer 1"); - Asset asset = createAsset(tenantId1, customer.getId(), "A", "Asset 1"); + AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile 1"); + Asset asset = createAsset(tenantId1, customer.getId(), assetProfile.getId(), "Asset 1"); RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1", asset.getId()); Dashboard dashboard = createDashboard(tenantId1, customer.getId(), "Dashboard 1", asset.getId()); + + assetProfile.setDefaultRuleChainId(ruleChain.getId()); + assetProfile.setDefaultDashboardId(dashboard.getId()); + assetProfile = assetProfileService.saveAssetProfile(assetProfile); + DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); Map ids = new HashMap<>(); - for (EntityId entityId : List.of(customer.getId(), asset.getId(), ruleChain.getId(), dashboard.getId(), + for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() @@ -546,6 +577,10 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest { ids.put(entityId, (EntityId) importResult.getSavedEntity().getId()); } + AssetProfile exportedAssetProfile = (AssetProfile) exportEntity(tenantAdmin2, (AssetProfileId) ids.get(assetProfile.getId())).getEntity(); + assertThat(exportedAssetProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); + assertThat(exportedAssetProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); + Asset exportedAsset = (Asset) exportEntity(tenantAdmin2, (AssetId) ids.get(asset.getId())).getEntity(); assertThat(exportedAsset.getCustomerId()).isEqualTo(customer.getId()); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java new file mode 100644 index 0000000000..c86463336a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.id.AssetProfileId; +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 AssetProfileService { + + AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId); + + AssetProfile findAssetProfileByName(TenantId tenantId, String profileName); + + AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId); + + AssetProfile saveAssetProfile(AssetProfile assetProfile); + + void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId); + + PageData findAssetProfiles(TenantId tenantId, PageLink pageLink); + + PageData findAssetProfileInfos(TenantId tenantId, PageLink pageLink); + + AssetProfile findOrCreateAssetProfile(TenantId tenantId, String profileName); + + AssetProfile createDefaultAssetProfile(TenantId tenantId); + + AssetProfile findDefaultAssetProfile(TenantId tenantId); + + AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId); + + boolean setDefaultAssetProfile(TenantId tenantId, AssetProfileId assetProfileId); + + void deleteAssetProfilesByTenantId(TenantId tenantId); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index b86abd81c7..955444ac52 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -21,15 +21,14 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; -import java.util.Optional; public interface AssetService { @@ -57,6 +56,8 @@ public interface AssetService { PageData findAssetInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink); + PageData findAssetInfosByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink); + ListenableFuture> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds); void deleteAssetsByTenantId(TenantId tenantId); @@ -69,6 +70,8 @@ public interface AssetService { PageData findAssetInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink); + PageData findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, PageLink pageLink); + ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds); void unassignCustomerAssets(TenantId tenantId, CustomerId customerId); 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 3d5578adf8..0c5ed80cd1 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 @@ -29,6 +29,8 @@ public class CacheConstants { public static final String TENANTS_CACHE = "tenants"; public static final String TENANTS_EXIST_CACHE = "tenantsExist"; public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; + + public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; public static final String USERS_UPDATE_TIME_CACHE = "usersUpdateTime"; public static final String OTA_PACKAGE_CACHE = "otaPackages"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java index 0155aecc96..44eff251c7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java @@ -42,6 +42,8 @@ public final class EdgeUtils { return EdgeEventType.DEVICE_PROFILE; case ASSET: return EdgeEventType.ASSET; + case ASSET_PROFILE: + return EdgeEventType.ASSET_PROFILE; case ENTITY_VIEW: return EdgeEventType.ENTITY_VIEW; case DASHBOARD: 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 1ef5bb433a..0798647903 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, OTA_PACKAGE, EDGE, RPC, QUEUE; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index f4ad335a43..96220ef656 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -27,7 +27,9 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -52,6 +54,8 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements @Length(fieldName = "label") private String label; + private AssetProfileId assetProfileId; + @Getter @Setter private AssetId externalId; @@ -70,6 +74,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.name = asset.getName(); this.type = asset.getType(); this.label = asset.getLabel(); + this.assetProfileId = asset.getAssetProfileId(); this.externalId = asset.getExternalId(); } @@ -79,6 +84,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.name = asset.getName(); this.type = asset.getType(); this.label = asset.getLabel(); + this.assetProfileId = asset.getAssetProfileId(); Optional.ofNullable(asset.getAdditionalInfo()).ifPresent(this::setAdditionalInfo); this.externalId = asset.getExternalId(); } @@ -144,12 +150,22 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.label = label; } + @ApiModelProperty(position = 8, required = true, value = "JSON object with Asset Profile Id.") + public AssetProfileId getAssetProfileId() { + return assetProfileId; + } + + public void setAssetProfileId(AssetProfileId assetProfileId) { + this.assetProfileId = assetProfileId; + } + + @Override public String getSearchText() { return getName(); } - @ApiModelProperty(position = 8, value = "Additional parameters of the asset", dataType = "com.fasterxml.jackson.databind.JsonNode") + @ApiModelProperty(position = 9, value = "Additional parameters of the asset", dataType = "com.fasterxml.jackson.databind.JsonNode") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); @@ -168,6 +184,8 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements builder.append(type); builder.append(", label="); builder.append(label); + builder.append(", assetProfileId="); + builder.append(assetProfileId); builder.append(", additionalInfo="); builder.append(getAdditionalInfo()); builder.append(", createdTime="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java index a6a532cbe6..df2d71d436 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java @@ -24,11 +24,15 @@ import org.thingsboard.server.common.data.id.AssetId; @Data public class AssetInfo extends Asset { - @ApiModelProperty(position = 9, value = "Title of the Customer that owns the asset.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 10, value = "Title of the Customer that owns the asset.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private String customerTitle; - @ApiModelProperty(position = 10, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @ApiModelProperty(position = 11, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) private boolean customerIsPublic; + @ApiModelProperty(position = 12, value = "Name of the corresponding Asset Profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String assetProfileName; + + public AssetInfo() { super(); } @@ -37,9 +41,10 @@ public class AssetInfo extends Asset { super(assetId); } - public AssetInfo(Asset asset, String customerTitle, boolean customerIsPublic) { + public AssetInfo(Asset asset, String customerTitle, boolean customerIsPublic, String assetProfileName) { super(asset); this.customerTitle = customerTitle; this.customerIsPublic = customerIsPublic; + this.assetProfileName = assetProfileName; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java new file mode 100644 index 0000000000..1bd9d9c6a5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfile.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.asset; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.SearchTextBased; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +@ApiModel +@Data +@ToString(exclude = {"image"}) +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class AssetProfile extends SearchTextBased implements HasName, HasTenantId, ExportableEntity { + + private static final long serialVersionUID = 6998485460273302018L; + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id that owns the profile.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private TenantId tenantId; + @NoXss + @Length(fieldName = "name") + @ApiModelProperty(position = 4, value = "Unique Asset Profile Name in scope of Tenant.", example = "Building") + private String name; + @NoXss + @ApiModelProperty(position = 11, value = "Asset Profile description. ") + private String description; + @Length(fieldName = "image", max = 1000000) + @ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ") + private String image; + private boolean isDefault; + @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + + "If present, the specified rule chain will be used to process all messages related to asset, including asset updates, telemetry, attribute updates, etc. " + + "Otherwise, the root rule chain will be used to process those messages.") + private RuleChainId defaultRuleChainId; + @ApiModelProperty(position = 6, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to asset details.") + private DashboardId defaultDashboardId; + + @NoXss + @ApiModelProperty(position = 8, value = "Rule engine queue name. " + + "If present, the specified queue will be used to store all unprocessed messages related to asset, including asset updates, telemetry, attribute updates, etc. " + + "Otherwise, the 'Main' queue will be used to store those messages.") + private String defaultQueueName; + + private AssetProfileId externalId; + + public AssetProfile() { + super(); + } + + public AssetProfile(AssetProfileId assetProfileId) { + super(assetProfileId); + } + + public AssetProfile(AssetProfile assetProfile) { + super(assetProfile); + this.tenantId = assetProfile.getTenantId(); + this.name = assetProfile.getName(); + this.description = assetProfile.getDescription(); + this.image = assetProfile.getImage(); + this.isDefault = assetProfile.isDefault(); + this.defaultRuleChainId = assetProfile.getDefaultRuleChainId(); + this.defaultDashboardId = assetProfile.getDefaultDashboardId(); + this.defaultQueueName = assetProfile.getDefaultQueueName(); + this.externalId = assetProfile.getExternalId(); + } + + @ApiModelProperty(position = 1, value = "JSON object with the asset profile Id. " + + "Specify this field to update the asset profile. " + + "Referencing non-existing asset profile Id will cause error. " + + "Omit this field to create new asset profile.") + @Override + public AssetProfileId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the profile creation, in milliseconds", example = "1609459200000", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @Override + public String getSearchText() { + return getName(); + } + + @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the asset profile is not specified during asset creation.") + public boolean isDefault(){ + return isDefault; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java new file mode 100644 index 0000000000..e2f37ad679 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.asset; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Value; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.util.UUID; + +@Value +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true, exclude = "image") +public class AssetProfileInfo extends EntityInfo { + + @ApiModelProperty(position = 3, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of asset profiles in the grid view. ") + private final String image; + @ApiModelProperty(position = 4, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to asset details.") + private final DashboardId defaultDashboardId; + + @JsonCreator + public AssetProfileInfo(@JsonProperty("id") EntityId id, + @JsonProperty("name") String name, + @JsonProperty("image") String image, + @JsonProperty("defaultDashboardId") DashboardId defaultDashboardId) { + super(id, name); + this.image = image; + this.defaultDashboardId = defaultDashboardId; + } + + public AssetProfileInfo(UUID uuid, String name, String image, UUID defaultDashboardId) { + super(EntityIdFactory.getByTypeAndUuid(EntityType.ASSET_PROFILE, uuid), name); + this.image = image; + this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; + } + + public AssetProfileInfo(AssetProfile profile) { + this(profile.getId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 660fa945ca..2403a4ebbf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -20,6 +20,7 @@ public enum EdgeEventType { ASSET, DEVICE, DEVICE_PROFILE, + ASSET_PROFILE, ENTITY_VIEW, ALARM, RULE_CHAIN, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java new file mode 100644 index 0000000000..46971d87c4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetProfileId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; +import org.thingsboard.server.common.data.EntityType; + +public class AssetProfileId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AssetProfileId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AssetProfileId fromString(String assetProfileId) { + return new AssetProfileId(UUID.fromString(assetProfileId)); + } + + @ApiModelProperty(position = 2, required = true, value = "string", example = "ASSET_PROFILE", allowableValues = "ASSET_PROFILE") + @Override + public EntityType getEntityType() { + return EntityType.ASSET_PROFILE; + } +} 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 f8bc8fc089..da9a5c49eb 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 @@ -65,6 +65,8 @@ public class EntityIdFactory { return new WidgetTypeId(uuid); case DEVICE_PROFILE: return new DeviceProfileId(uuid); + case ASSET_PROFILE: + return new AssetProfileId(uuid); case TENANT_PROFILE: return new TenantProfileId(uuid); case API_USAGE_STATE: @@ -95,6 +97,8 @@ public class EntityIdFactory { return new DeviceId(uuid); case DEVICE_PROFILE: return new DeviceProfileId(uuid); + case ASSET_PROFILE: + return new AssetProfileId(uuid); case ASSET: return new AssetId(uuid); case ALARM: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 0e2929bef8..8dccecbc29 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.widget.WidgetsBundle; @@ -41,6 +42,7 @@ import java.lang.annotation.Target; @Type(name = "DEVICE", value = Device.class), @Type(name = "RULE_CHAIN", value = RuleChain.class), @Type(name = "DEVICE_PROFILE", value = DeviceProfile.class), + @Type(name = "ASSET_PROFILE", value = AssetProfile.class), @Type(name = "ASSET", value = Asset.class), @Type(name = "DASHBOARD", value = Dashboard.class), @Type(name = "CUSTOMER", value = Customer.class), diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 7272e6f96a..04d447cebf 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -223,6 +223,19 @@ message DeviceProfileUpdateMsg { optional int64 firmwareIdLSB = 17; } +message AssetProfileUpdateMsg { + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string name = 4; + optional string description = 5; + bool default = 6; + int64 defaultRuleChainIdMSB = 7; + int64 defaultRuleChainIdLSB = 8; + string defaultQueueName = 9; + optional bytes image = 10; +} + message DeviceCredentialsUpdateMsg { int64 deviceIdMSB = 1; int64 deviceIdLSB = 2; @@ -237,10 +250,12 @@ message AssetUpdateMsg { int64 idLSB = 3; optional int64 customerIdMSB = 4; optional int64 customerIdLSB = 5; - string name = 6; - string type = 7; - optional string label = 8; - optional string additionalInfo = 9; + optional int64 assetProfileIdMSB = 6; + optional int64 assetProfileIdLSB = 7; + string name = 8; + string type = 9; + optional string label = 10; + optional string additionalInfo = 11; } message EntityViewUpdateMsg { @@ -389,6 +404,11 @@ message DeviceProfileDevicesRequestMsg { int64 deviceProfileIdLSB = 2; } +message AssetProfileAssetsRequestMsg { + int64 assetProfileIdMSB = 1; + int64 assetProfileIdLSB = 2; +} + message WidgetBundleTypesRequestMsg { int64 widgetBundleIdMSB = 1; int64 widgetBundleIdLSB = 2; @@ -496,6 +516,7 @@ message UplinkMsg { repeated DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg = 13; repeated WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg = 14; repeated EntityViewsRequestMsg entityViewsRequestMsg = 15; + repeated AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg = 16; } message UplinkResponseMsg { @@ -534,5 +555,6 @@ message DownlinkMsg { repeated DeviceRpcCallMsg deviceRpcCallMsg = 21; repeated OtaPackageUpdateMsg otaPackageUpdateMsg = 22; repeated QueueUpdateMsg queueUpdateMsg = 23; + repeated AssetProfileUpdateMsg assetProfileUpdateMsg = 24; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java index e3c51b48a1..4d67a794b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java @@ -92,6 +92,16 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD */ PageData findAssetInfosByTenantIdAndType(UUID tenantId, String type, PageLink pageLink); + /** + * Find asset infos by tenantId, assetProfileId and page link. + * + * @param tenantId the tenantId + * @param assetProfileId the assetProfileId + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantIdAndAssetProfileId(UUID tenantId, UUID assetProfileId, PageLink pageLink); + /** * Find assets by tenantId and assets Ids. * @@ -143,6 +153,17 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD */ PageData findAssetInfosByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink); + /** + * Find asset infos by tenantId, customerId, assetProfileId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param assetProfileId the assetProfileId + * @param pageLink the page link + * @return the list of asset info objects + */ + PageData findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(UUID tenantId, UUID customerId, UUID assetProfileId, PageLink pageLink); + /** * Find assets by tenantId, customerId and assets Ids. * @@ -169,6 +190,18 @@ public interface AssetDao extends Dao, TenantEntityDao, ExportableEntityD */ ListenableFuture> findTenantAssetTypesAsync(UUID tenantId); + Long countAssetsByAssetProfileId(TenantId tenantId, UUID assetProfileId); + + /** + * Find assets by tenantId, profileId and page link. + * + * @param tenantId the tenantId + * @param profileId the profileId + * @param pageLink the page link + * @return the list of device objects + */ + PageData findAssetsByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink); + /** * Find assets by tenantId, edgeId and page link. * diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java new file mode 100644 index 0000000000..2950dca45e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCacheKey.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Data; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.io.Serializable; + +@Data +public class AssetProfileCacheKey implements Serializable { + + private static final long serialVersionUID = 8220455917177676472L; + + private final TenantId tenantId; + private final String name; + private final AssetProfileId assetProfileId; + private final boolean defaultProfile; + + private AssetProfileCacheKey(TenantId tenantId, String name, AssetProfileId assetProfileId, boolean defaultProfile) { + this.tenantId = tenantId; + this.name = name; + this.assetProfileId = assetProfileId; + this.defaultProfile = defaultProfile; + } + + public static AssetProfileCacheKey fromName(TenantId tenantId, String name) { + return new AssetProfileCacheKey(tenantId, name, null, false); + } + + public static AssetProfileCacheKey fromId(AssetProfileId id) { + return new AssetProfileCacheKey(null, null, id, false); + } + + public static AssetProfileCacheKey defaultProfile(TenantId tenantId) { + return new AssetProfileCacheKey(tenantId, null, null, true); + } + + @Override + public String toString() { + if (assetProfileId != null) { + return assetProfileId.toString(); + } else if (defaultProfile) { + return tenantId.toString(); + } else { + return tenantId + "_" + name; + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCaffeineCache.java new file mode 100644 index 0000000000..02c4ece8bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.AssetProfile; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("AssetProfileCache") +public class AssetProfileCaffeineCache extends CaffeineTbTransactionalCache { + + public AssetProfileCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.ASSET_PROFILE_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java new file mode 100644 index 0000000000..4e36aa5023 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileDao.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.id.AssetProfileId; +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 org.thingsboard.server.dao.ExportableEntityDao; + +import java.util.UUID; + +public interface AssetProfileDao extends Dao, ExportableEntityDao { + + AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, UUID assetProfileId); + + AssetProfile save(TenantId tenantId, AssetProfile assetProfile); + + AssetProfile saveAndFlush(TenantId tenantId, AssetProfile assetProfile); + + PageData findAssetProfiles(TenantId tenantId, PageLink pageLink); + + PageData findAssetProfileInfos(TenantId tenantId, PageLink pageLink); + + AssetProfile findDefaultAssetProfile(TenantId tenantId); + + AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId); + + AssetProfile findByName(TenantId tenantId, String profileName); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileEvictEvent.java new file mode 100644 index 0000000000..880ca83b7d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileEvictEvent.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import lombok.Data; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class AssetProfileEvictEvent { + + private final TenantId tenantId; + private final String newName; + private final String oldName; + private final AssetProfileId assetProfileId; + private final boolean defaultProfile; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileRedisCache.java new file mode 100644 index 0000000000..ed917317ad --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileRedisCache.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.AssetProfile; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("AssetProfileCache") +public class AssetProfileRedisCache extends RedisTbTransactionalCache { + + public AssetProfileRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.ASSET_PROFILE_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java new file mode 100644 index 0000000000..3b0f2d32e6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -0,0 +1,286 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.asset; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.id.AssetProfileId; +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.entity.AbstractCachedEntityService; +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.service.Validator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class AssetProfileServiceImpl extends AbstractCachedEntityService implements AssetProfileService { + + private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + private static final String INCORRECT_ASSET_PROFILE_ID = "Incorrect assetProfileId "; + + private static final String INCORRECT_ASSET_PROFILE_NAME = "Incorrect assetProfileName "; + + private static final String ASSET_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS = "Asset profile with such name already exists!"; + + @Autowired + private AssetProfileDao assetProfileDao; + + @Autowired + private AssetDao assetDao; + + @Autowired + private AssetService assetService; + + @Autowired + private DataValidator assetProfileValidator; + + @TransactionalEventListener(classes = AssetProfileEvictEvent.class) + @Override + public void handleEvictEvent(AssetProfileEvictEvent event) { + List keys = new ArrayList<>(2); + keys.add(AssetProfileCacheKey.fromName(event.getTenantId(), event.getNewName())); + if (event.getAssetProfileId() != null) { + keys.add(AssetProfileCacheKey.fromId(event.getAssetProfileId())); + } + if (event.isDefaultProfile()) { + keys.add(AssetProfileCacheKey.defaultProfile(event.getTenantId())); + } + if (StringUtils.isNotEmpty(event.getOldName()) && !event.getOldName().equals(event.getNewName())) { + keys.add(AssetProfileCacheKey.fromName(event.getTenantId(), event.getOldName())); + } + cache.evict(keys); + } + + @Override + public AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId) { + log.trace("Executing findAssetProfileById [{}]", assetProfileId); + Validator.validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + return cache.getAndPutInTransaction(AssetProfileCacheKey.fromId(assetProfileId), + () -> assetProfileDao.findById(tenantId, assetProfileId.getId()), true); + } + + @Override + public AssetProfile findAssetProfileByName(TenantId tenantId, String profileName) { + log.trace("Executing findAssetProfileByName [{}][{}]", tenantId, profileName); + Validator.validateString(profileName, INCORRECT_ASSET_PROFILE_NAME + profileName); + return cache.getAndPutInTransaction(AssetProfileCacheKey.fromName(tenantId, profileName), + () -> assetProfileDao.findByName(tenantId, profileName), true); + } + + @Override + public AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId) { + log.trace("Executing findAssetProfileInfoById [{}]", assetProfileId); + Validator.validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + return toAssetProfileInfo(findAssetProfileById(tenantId, assetProfileId)); + } + + @Override + public AssetProfile saveAssetProfile(AssetProfile assetProfile) { + log.trace("Executing saveAssetProfile [{}]", assetProfile); + AssetProfile oldAssetProfile = assetProfileValidator.validate(assetProfile, AssetProfile::getTenantId); + AssetProfile savedAssetProfile; + try { + savedAssetProfile = assetProfileDao.saveAndFlush(assetProfile.getTenantId(), assetProfile); + publishEvictEvent(new AssetProfileEvictEvent(savedAssetProfile.getTenantId(), savedAssetProfile.getName(), + oldAssetProfile != null ? oldAssetProfile.getName() : null, savedAssetProfile.getId(), savedAssetProfile.isDefault())); + } catch (Exception t) { + handleEvictEvent(new AssetProfileEvictEvent(assetProfile.getTenantId(), assetProfile.getName(), + oldAssetProfile != null ? oldAssetProfile.getName() : null, null, assetProfile.isDefault())); + checkConstraintViolation(t, + Map.of("asset_profile_name_unq_key", ASSET_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS, + "asset_profile_external_id_unq_key", "Asset profile with such external id already exists!")); + throw t; + } + if (oldAssetProfile != null && !oldAssetProfile.getName().equals(assetProfile.getName())) { + PageLink pageLink = new PageLink(100); + PageData pageData; + do { + pageData = assetDao.findAssetsByTenantIdAndProfileId(assetProfile.getTenantId().getId(), assetProfile.getUuidId(), pageLink); + for (Asset asset : pageData.getData()) { + asset.setType(assetProfile.getName()); + assetService.saveAsset(asset); + } + pageLink = pageLink.nextPageLink(); + } while (pageData.hasNext()); + } + return savedAssetProfile; + } + + @Override + @Transactional + public void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId) { + log.trace("Executing deleteAssetProfile [{}]", assetProfileId); + Validator.validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + AssetProfile assetProfile = assetProfileDao.findById(tenantId, assetProfileId.getId()); + if (assetProfile != null && assetProfile.isDefault()) { + throw new DataValidationException("Deletion of Default Asset Profile is prohibited!"); + } + this.removeAssetProfile(tenantId, assetProfile); + } + + private void removeAssetProfile(TenantId tenantId, AssetProfile assetProfile) { + AssetProfileId assetProfileId = assetProfile.getId(); + try { + deleteEntityRelations(tenantId, assetProfileId); + assetProfileDao.removeById(tenantId, assetProfileId.getId()); + publishEvictEvent(new AssetProfileEvictEvent(assetProfile.getTenantId(), assetProfile.getName(), + null, assetProfile.getId(), assetProfile.isDefault())); + } catch (Exception t) { + ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); + if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("fk_asset_profile")) { + throw new DataValidationException("The asset profile referenced by the assets cannot be deleted!"); + } else { + throw t; + } + } + } + + @Override + public PageData findAssetProfiles(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAssetProfiles tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return assetProfileDao.findAssetProfiles(tenantId, pageLink); + } + + @Override + public PageData findAssetProfileInfos(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findAssetProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return assetProfileDao.findAssetProfileInfos(tenantId, pageLink); + } + + @Override + public AssetProfile findOrCreateAssetProfile(TenantId tenantId, String name) { + log.trace("Executing findOrCreateAssetProfile"); + AssetProfile assetProfile = findAssetProfileByName(tenantId, name); + if (assetProfile == null) { + try { + assetProfile = this.doCreateDefaultAssetProfile(tenantId, name, name.equals("default")); + } catch (DataValidationException e) { + if (ASSET_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) { + assetProfile = findAssetProfileByName(tenantId, name); + } else { + throw e; + } + } + } + return assetProfile; + } + + @Override + public AssetProfile createDefaultAssetProfile(TenantId tenantId) { + log.trace("Executing createDefaultAssetProfile tenantId [{}]", tenantId); + return doCreateDefaultAssetProfile(tenantId, "default", true); + } + + private AssetProfile doCreateDefaultAssetProfile(TenantId tenantId, String profileName, boolean defaultProfile) { + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfile.setDefault(defaultProfile); + assetProfile.setName(profileName); + assetProfile.setDescription("Default asset profile"); + return saveAssetProfile(assetProfile); + } + + @Override + public AssetProfile findDefaultAssetProfile(TenantId tenantId) { + log.trace("Executing findDefaultAssetProfile tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return cache.getAndPutInTransaction(AssetProfileCacheKey.defaultProfile(tenantId), + () -> assetProfileDao.findDefaultAssetProfile(tenantId), true); + } + + @Override + public AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId) { + log.trace("Executing findDefaultAssetProfileInfo tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return toAssetProfileInfo(findDefaultAssetProfile(tenantId)); + } + + @Override + public boolean setDefaultAssetProfile(TenantId tenantId, AssetProfileId assetProfileId) { + log.trace("Executing setDefaultAssetProfile [{}]", assetProfileId); + Validator.validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + AssetProfile assetProfile = assetProfileDao.findById(tenantId, assetProfileId.getId()); + if (!assetProfile.isDefault()) { + assetProfile.setDefault(true); + AssetProfile previousDefaultAssetProfile = findDefaultAssetProfile(tenantId); + boolean changed = false; + if (previousDefaultAssetProfile == null) { + assetProfileDao.save(tenantId, assetProfile); + publishEvictEvent(new AssetProfileEvictEvent(assetProfile.getTenantId(), assetProfile.getName(), null, assetProfile.getId(), true)); + changed = true; + } else if (!previousDefaultAssetProfile.getId().equals(assetProfile.getId())) { + previousDefaultAssetProfile.setDefault(false); + assetProfileDao.save(tenantId, previousDefaultAssetProfile); + assetProfileDao.save(tenantId, assetProfile); + publishEvictEvent(new AssetProfileEvictEvent(previousDefaultAssetProfile.getTenantId(), previousDefaultAssetProfile.getName(), null, previousDefaultAssetProfile.getId(), false)); + publishEvictEvent(new AssetProfileEvictEvent(assetProfile.getTenantId(), assetProfile.getName(), null, assetProfile.getId(), true)); + changed = true; + } + return changed; + } + return false; + } + + @Override + public void deleteAssetProfilesByTenantId(TenantId tenantId) { + log.trace("Executing deleteAssetProfilesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantAssetProfilesRemover.removeEntities(tenantId, tenantId); + } + + private PaginatedRemover tenantAssetProfilesRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return assetProfileDao.findAssetProfiles(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, AssetProfile entity) { + removeAssetProfile(tenantId, entity); + } + }; + + private AssetProfileInfo toAssetProfileInfo(AssetProfile profile) { + return profile == null ? null : new AssetProfileInfo(profile.getId(), profile.getName(), profile.getImage(), + profile.getDefaultDashboardId()); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 3ffcc999c3..15d6df1327 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -30,9 +30,11 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; @@ -64,6 +66,8 @@ import static org.thingsboard.server.dao.service.Validator.validateString; public class BaseAssetService extends AbstractCachedEntityService implements AssetService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + public static final String INCORRECT_ASSET_PROFILE_ID = "Incorrect assetProfileId "; public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; public static final String INCORRECT_ASSET_ID = "Incorrect assetId "; public static final String TB_SERVICE_QUEUE = "TbServiceQueue"; @@ -71,6 +75,9 @@ public class BaseAssetService extends AbstractCachedEntityService assetValidator; @@ -122,6 +129,24 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantIdAndAssetProfileId, tenantId [{}], assetProfileId [{}], pageLink [{}]", tenantId, assetProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantIdAndAssetProfileId(tenantId.getId(), assetProfileId.getId(), pageLink); + } + @Override public ListenableFuture> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List assetIds) { log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds); @@ -253,6 +287,16 @@ public class BaseAssetService extends AbstractCachedEntityService findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, PageLink pageLink) { + log.trace("Executing findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId, tenantId [{}], customerId [{}], assetProfileId [{}], pageLink [{}]", tenantId, customerId, assetProfileId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validateId(assetProfileId, INCORRECT_ASSET_PROFILE_ID + assetProfileId); + validatePageLink(pageLink); + return assetDao.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId.getId(), customerId.getId(), assetProfileId.getId(), pageLink); + } + @Override public ListenableFuture> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List assetIds) { log.trace("Executing findAssetsByTenantIdAndCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 3a1520aa4f..5985674bd0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -402,6 +402,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService extends BaseSqlEntity @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; + @Column(name = ModelConstants.ASSET_ASSET_PROFILE_ID_PROPERTY, columnDefinition = "uuid") + private UUID assetProfileId; + @Column(name = EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -87,6 +91,9 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity if (asset.getCustomerId() != null) { this.customerId = asset.getCustomerId().getId(); } + if (asset.getAssetProfileId() != null) { + this.assetProfileId = asset.getAssetProfileId().getId(); + } this.name = asset.getName(); this.type = asset.getType(); this.label = asset.getLabel(); @@ -101,6 +108,7 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity this.setCreatedTime(assetEntity.getCreatedTime()); this.tenantId = assetEntity.getTenantId(); this.customerId = assetEntity.getCustomerId(); + this.assetProfileId = assetEntity.getAssetProfileId(); this.type = assetEntity.getType(); this.name = assetEntity.getName(); this.label = assetEntity.getLabel(); @@ -132,6 +140,9 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity if (customerId != null) { asset.setCustomerId(new CustomerId(customerId)); } + if (assetProfileId != null) { + asset.setAssetProfileId(new AssetProfileId(assetProfileId)); + } asset.setName(name); asset.setType(type); asset.setLabel(label); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java index a0c4dd0563..bdcf1bce35 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetInfoEntity.java @@ -30,10 +30,12 @@ public class AssetInfoEntity extends AbstractAssetEntity { public static final Map assetInfoColumnMap = new HashMap<>(); static { assetInfoColumnMap.put("customerTitle", "c.title"); + assetInfoColumnMap.put("assetProfileName", "p.name"); } private String customerTitle; private boolean customerIsPublic; + private String assetProfileName; public AssetInfoEntity() { super(); @@ -41,7 +43,8 @@ public class AssetInfoEntity extends AbstractAssetEntity { public AssetInfoEntity(AssetEntity assetEntity, String customerTitle, - Object customerAdditionalInfo) { + Object customerAdditionalInfo, + String assetProfileName) { super(assetEntity); this.customerTitle = customerTitle; if (customerAdditionalInfo != null && ((JsonNode)customerAdditionalInfo).has("isPublic")) { @@ -49,10 +52,11 @@ public class AssetInfoEntity extends AbstractAssetEntity { } else { this.customerIsPublic = false; } + this.assetProfileName = assetProfileName; } @Override public AssetInfo toData() { - return new AssetInfo(super.toAsset(), customerTitle, customerIsPublic); + return new AssetInfo(super.toAsset(), customerTitle, customerIsPublic, assetProfileName); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java new file mode 100644 index 0000000000..36f7b40487 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetProfileEntity.java @@ -0,0 +1,136 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.RuleChainId; +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 javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.ASSET_PROFILE_COLUMN_FAMILY_NAME) +public final class AssetProfileEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.ASSET_PROFILE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.ASSET_PROFILE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.ASSET_PROFILE_IMAGE_PROPERTY) + private String image; + + @Column(name = ModelConstants.ASSET_PROFILE_DESCRIPTION_PROPERTY) + private String description; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Column(name = ModelConstants.ASSET_PROFILE_IS_DEFAULT_PROPERTY) + private boolean isDefault; + + @Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_RULE_CHAIN_ID_PROPERTY, columnDefinition = "uuid") + private UUID defaultRuleChainId; + + @Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_DASHBOARD_ID_PROPERTY) + private UUID defaultDashboardId; + + @Column(name = ModelConstants.ASSET_PROFILE_DEFAULT_QUEUE_NAME_PROPERTY) + private String defaultQueueName; + + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + + public AssetProfileEntity() { + super(); + } + + public AssetProfileEntity(AssetProfile assetProfile) { + if (assetProfile.getId() != null) { + this.setUuid(assetProfile.getId().getId()); + } + if (assetProfile.getTenantId() != null) { + this.tenantId = assetProfile.getTenantId().getId(); + } + this.setCreatedTime(assetProfile.getCreatedTime()); + this.name = assetProfile.getName(); + this.image = assetProfile.getImage(); + this.description = assetProfile.getDescription(); + this.isDefault = assetProfile.isDefault(); + if (assetProfile.getDefaultRuleChainId() != null) { + this.defaultRuleChainId = assetProfile.getDefaultRuleChainId().getId(); + } + if (assetProfile.getDefaultDashboardId() != null) { + this.defaultDashboardId = assetProfile.getDefaultDashboardId().getId(); + } + this.defaultQueueName = assetProfile.getDefaultQueueName(); + if (assetProfile.getExternalId() != null) { + this.externalId = assetProfile.getExternalId().getId(); + } + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + public String getSearchText() { + return searchText; + } + + @Override + public AssetProfile toData() { + AssetProfile assetProfile = new AssetProfile(new AssetProfileId(this.getUuid())); + assetProfile.setCreatedTime(createdTime); + if (tenantId != null) { + assetProfile.setTenantId(TenantId.fromUUID(tenantId)); + } + assetProfile.setName(name); + assetProfile.setImage(image); + assetProfile.setDescription(description); + assetProfile.setDefault(isDefault); + assetProfile.setDefaultQueueName(defaultQueueName); + if (defaultRuleChainId != null) { + assetProfile.setDefaultRuleChainId(new RuleChainId(defaultRuleChainId)); + } + if (defaultDashboardId != null) { + assetProfile.setDefaultDashboardId(new DashboardId(defaultDashboardId)); + } + if (externalId != null) { + assetProfile.setExternalId(new AssetProfileId(externalId)); + } + + return assetProfile; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index b7685a0476..6c9e7792ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -73,9 +73,6 @@ public class AssetDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Asset asset) { - if (StringUtils.isEmpty(asset.getType())) { - throw new DataValidationException("Asset type should be specified!"); - } if (StringUtils.isEmpty(asset.getName())) { throw new DataValidationException("Asset name should be specified!"); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java new file mode 100644 index 0000000000..c880474629 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -0,0 +1,109 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.validator; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.dao.asset.AssetProfileDao; +import org.thingsboard.server.dao.asset.AssetProfileService; +import org.thingsboard.server.dao.dashboard.DashboardService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.queue.QueueService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; + +@Component +public class AssetProfileDataValidator extends DataValidator { + + @Autowired + private AssetProfileDao assetProfileDao; + @Autowired + @Lazy + private AssetProfileService assetProfileService; + @Autowired + private TenantService tenantService; + @Lazy + @Autowired + private QueueService queueService; + @Autowired + private RuleChainService ruleChainService; + @Autowired + private DashboardService dashboardService; + + @Override + protected void validateDataImpl(TenantId tenantId, AssetProfile assetProfile) { + if (StringUtils.isEmpty(assetProfile.getName())) { + throw new DataValidationException("Asset profile name should be specified!"); + } + if (assetProfile.getTenantId() == null) { + throw new DataValidationException("Asset profile should be assigned to tenant!"); + } else { + if (!tenantService.tenantExists(assetProfile.getTenantId())) { + throw new DataValidationException("Asset profile is referencing to non-existent tenant!"); + } + } + if (assetProfile.isDefault()) { + AssetProfile defaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId); + if (defaultAssetProfile != null && !defaultAssetProfile.getId().equals(assetProfile.getId())) { + throw new DataValidationException("Another default asset profile is present in scope of current tenant!"); + } + } + if (StringUtils.isNotEmpty(assetProfile.getDefaultQueueName())) { + Queue queue = queueService.findQueueByTenantIdAndName(tenantId, assetProfile.getDefaultQueueName()); + if (queue == null) { + throw new DataValidationException("Asset profile is referencing to non-existent queue!"); + } + } + + if (assetProfile.getDefaultRuleChainId() != null) { + RuleChain ruleChain = ruleChainService.findRuleChainById(tenantId, assetProfile.getDefaultRuleChainId()); + if (ruleChain == null) { + throw new DataValidationException("Can't assign non-existent rule chain!"); + } + if (!ruleChain.getTenantId().equals(assetProfile.getTenantId())) { + throw new DataValidationException("Can't assign rule chain from different tenant!"); + } + } + + if (assetProfile.getDefaultDashboardId() != null) { + DashboardInfo dashboard = dashboardService.findDashboardInfoById(tenantId, assetProfile.getDefaultDashboardId()); + if (dashboard == null) { + throw new DataValidationException("Can't assign non-existent dashboard!"); + } + if (!dashboard.getTenantId().equals(assetProfile.getTenantId())) { + throw new DataValidationException("Can't assign dashboard from different tenant!"); + } + } + } + + @Override + protected AssetProfile validateUpdate(TenantId tenantId, AssetProfile assetProfile) { + AssetProfile old = assetProfileDao.findById(assetProfile.getTenantId(), assetProfile.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing asset profile!"); + } + return old; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java new file mode 100644 index 0000000000..34545fb04c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetProfileRepository.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.asset; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.dao.ExportableEntityRepository; +import org.thingsboard.server.dao.model.sql.AssetProfileEntity; + +import java.util.UUID; + +public interface AssetProfileRepository extends JpaRepository, ExportableEntityRepository { + + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a " + + "WHERE a.id = :assetProfileId") + AssetProfileInfo findAssetProfileInfoById(@Param("assetProfileId") UUID assetProfileId); + + @Query("SELECT a FROM AssetProfileEntity a WHERE " + + "a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + Page findAssetProfiles(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a WHERE " + + "a.tenantId = :tenantId AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + Page findAssetProfileInfos(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + @Query("SELECT a FROM AssetProfileEntity a " + + "WHERE a.tenantId = :tenantId AND a.isDefault = true") + AssetProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId); + + @Query("SELECT new org.thingsboard.server.common.data.asset.AssetProfileInfo(a.id, a.name, a.image, a.defaultDashboardId) " + + "FROM AssetProfileEntity a " + + "WHERE a.tenantId = :tenantId AND a.isDefault = true") + AssetProfileInfo findDefaultAssetProfileInfo(@Param("tenantId") UUID tenantId); + + AssetProfileEntity findByTenantIdAndName(UUID id, String profileName); + + @Query("SELECT externalId FROM AssetProfileEntity WHERE id = :id") + UUID getExternalIdById(@Param("id") UUID id); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index 6d4667d8ec..eadf0d2b84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -32,9 +32,10 @@ import java.util.UUID; */ public interface AssetRepository extends JpaRepository, ExportableEntityRepository { - @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.id = :assetId") AssetInfoEntity findAssetInfoById(@Param("assetId") UUID assetId); @@ -44,11 +45,15 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(p.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -61,9 +66,18 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + + "AND a.assetProfileId = :profileId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") + Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, + @Param("profileId") UUID profileId, + @Param("searchText") String searchText, + Pageable pageable); + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") @@ -86,17 +100,34 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.type = :type " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") Page findAssetInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + + "WHERE a.tenantId = :tenantId " + + "AND a.assetProfileId = :assetProfileId " + + "AND (LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(a.label) LIKE LOWER(CONCAT('%', :textSearch, '%')) " + + "OR LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%')))") + Page findAssetInfosByTenantIdAndAssetProfileId(@Param("tenantId") UUID tenantId, + @Param("assetProfileId") UUID assetProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId AND a.type = :type " + @@ -107,9 +138,10 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo) " + + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + "FROM AssetEntity a " + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + "WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + "AND a.type = :type " + @@ -120,9 +152,25 @@ public interface AssetRepository extends JpaRepository, Expor @Param("textSearch") String textSearch, Pageable pageable); + @Query("SELECT new org.thingsboard.server.dao.model.sql.AssetInfoEntity(a, c.title, c.additionalInfo, p.name) " + + "FROM AssetEntity a " + + "LEFT JOIN CustomerEntity c on c.id = a.customerId " + + "LEFT JOIN AssetProfileEntity p on p.id = a.assetProfileId " + + "WHERE a.tenantId = :tenantId " + + "AND a.customerId = :customerId " + + "AND a.assetProfileId = :assetProfileId " + + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") + Page findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(@Param("tenantId") UUID tenantId, + @Param("customerId") UUID customerId, + @Param("assetProfileId") UUID assetProfileId, + @Param("textSearch") String textSearch, + Pageable pageable); + @Query("SELECT DISTINCT a.type FROM AssetEntity a WHERE a.tenantId = :tenantId") List findTenantAssetTypes(@Param("tenantId") UUID tenantId); + Long countByAssetProfileId(UUID assetProfileId); + @Query("SELECT a FROM AssetEntity a, RelationEntity re WHERE a.tenantId = :tenantId " + "AND a.id = re.toId AND re.toType = 'ASSET' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index b18d6c5f51..181ed7fc7b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -144,6 +144,16 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } + @Override + public PageData findAssetInfosByTenantIdAndAssetProfileId(UUID tenantId, UUID assetProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantIdAndAssetProfileId( + tenantId, + assetProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); + } + @Override public PageData findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, PageLink pageLink) { return DaoUtil.toPageData(assetRepository @@ -166,11 +176,37 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); } + @Override + public PageData findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(UUID tenantId, UUID customerId, UUID assetProfileId, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId( + tenantId, + customerId, + assetProfileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink, AssetInfoEntity.assetInfoColumnMap))); + } + @Override public ListenableFuture> findTenantAssetTypesAsync(UUID tenantId) { return service.submit(() -> convertTenantAssetTypesToDto(tenantId, assetRepository.findTenantAssetTypes(tenantId))); } + @Override + public Long countAssetsByAssetProfileId(TenantId tenantId, UUID assetProfileId) { + return assetRepository.countByAssetProfileId(assetProfileId); + } + + @Override + public PageData findAssetsByTenantIdAndProfileId(UUID tenantId, UUID profileId, PageLink pageLink) { + return DaoUtil.toPageData( + assetRepository.findByTenantIdAndProfileId( + tenantId, + profileId, + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + private List convertTenantAssetTypesToDto(UUID tenantId, List types) { List list = Collections.emptyList(); if (types != null && !types.isEmpty()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java new file mode 100644 index 0000000000..9f71b97da2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql.asset; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.asset.AssetProfileInfo; +import org.thingsboard.server.common.data.id.AssetProfileId; +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.asset.AssetProfileDao; +import org.thingsboard.server.dao.model.sql.AssetProfileEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +@Component +public class JpaAssetProfileDao extends JpaAbstractSearchTextDao implements AssetProfileDao { + + @Autowired + private AssetProfileRepository assetProfileRepository; + + @Override + protected Class getEntityClass() { + return AssetProfileEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return assetProfileRepository; + } + + @Override + public AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, UUID assetProfileId) { + return assetProfileRepository.findAssetProfileInfoById(assetProfileId); + } + + @Transactional + @Override + public AssetProfile saveAndFlush(TenantId tenantId, AssetProfile assetProfile) { + AssetProfile result = save(tenantId, assetProfile); + assetProfileRepository.flush(); + return result; + } + + @Override + public PageData findAssetProfiles(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData( + assetProfileRepository.findAssetProfiles( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAssetProfileInfos(TenantId tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData( + assetProfileRepository.findAssetProfileInfos( + tenantId.getId(), + Objects.toString(pageLink.getTextSearch(), ""), + DaoUtil.toPageable(pageLink))); + } + + @Override + public AssetProfile findDefaultAssetProfile(TenantId tenantId) { + return DaoUtil.getData(assetProfileRepository.findByDefaultTrueAndTenantId(tenantId.getId())); + } + + @Override + public AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId) { + return assetProfileRepository.findDefaultAssetProfileInfo(tenantId.getId()); + } + + @Override + public AssetProfile findByName(TenantId tenantId, String profileName) { + return DaoUtil.getData(assetProfileRepository.findByTenantIdAndName(tenantId.getId(), profileName)); + } + + @Override + public AssetProfile findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(assetProfileRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public AssetProfile findByTenantIdAndName(UUID tenantId, String name) { + return DaoUtil.getData(assetProfileRepository.findByTenantIdAndName(tenantId, name)); + } + + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return findAssetProfiles(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public AssetProfileId getExternalIdByInternal(AssetProfileId internalId) { + return Optional.ofNullable(assetProfileRepository.getExternalIdById(internalId.getId())) + .map(AssetProfileId::new).orElse(null); + } + + @Override + public EntityType getEntityType() { + return EntityType.ASSET_PROFILE; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index bc7940f300..1963190aa5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -240,6 +240,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { entityTableMap.put(EntityType.EDGE, "edge"); entityTableMap.put(EntityType.RULE_CHAIN, "rule_chain"); entityTableMap.put(EntityType.DEVICE_PROFILE, "device_profile"); + entityTableMap.put(EntityType.ASSET_PROFILE, "asset_profile"); } public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 649d3e7f2c..e9a3d19715 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -103,6 +103,7 @@ public class EntityKeyMapping { allowedEntityFieldMap.put(EntityType.WIDGETS_BUNDLE, new HashSet<>(widgetEntityFields)); allowedEntityFieldMap.put(EntityType.API_USAGE_STATE, apiUsageStateEntityFields); allowedEntityFieldMap.put(EntityType.DEVICE_PROFILE, Set.of(CREATED_TIME, NAME, TYPE)); + allowedEntityFieldMap.put(EntityType.ASSET_PROFILE, Set.of(CREATED_TIME, NAME)); entityFieldColumnMap.put(CREATED_TIME, ModelConstants.CREATED_TIME_PROPERTY); entityFieldColumnMap.put(ENTITY_TYPE, ModelConstants.ENTITY_TYPE_PROPERTY); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 25489f3e1a..7c5d951c2a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -76,6 +77,9 @@ public class TenantServiceImpl extends AbstractCachedEntityService idComparator = new IdComparator<>(); + private IdComparator assetProfileInfoIdComparator = 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 testSaveAssetProfile() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); + Assert.assertNotNull(savedAssetProfile); + Assert.assertNotNull(savedAssetProfile.getId()); + Assert.assertTrue(savedAssetProfile.getCreatedTime() > 0); + Assert.assertEquals(assetProfile.getName(), savedAssetProfile.getName()); + Assert.assertEquals(assetProfile.getDescription(), savedAssetProfile.getDescription()); + Assert.assertEquals(assetProfile.isDefault(), savedAssetProfile.isDefault()); + Assert.assertEquals(assetProfile.getDefaultRuleChainId(), savedAssetProfile.getDefaultRuleChainId()); + savedAssetProfile.setName("New asset profile"); + assetProfileService.saveAssetProfile(savedAssetProfile); + AssetProfile foundAssetProfile = assetProfileService.findAssetProfileById(tenantId, savedAssetProfile.getId()); + Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfile.getName()); + } + + @Test + public void testFindAssetProfileById() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); + AssetProfile foundAssetProfile = assetProfileService.findAssetProfileById(tenantId, savedAssetProfile.getId()); + Assert.assertNotNull(foundAssetProfile); + Assert.assertEquals(savedAssetProfile, foundAssetProfile); + } + + @Test + public void testFindAssetProfileInfoById() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); + AssetProfileInfo foundAssetProfileInfo = assetProfileService.findAssetProfileInfoById(tenantId, savedAssetProfile.getId()); + Assert.assertNotNull(foundAssetProfileInfo); + Assert.assertEquals(savedAssetProfile.getId(), foundAssetProfileInfo.getId()); + Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName()); + } + + @Test + public void testFindDefaultAssetProfile() { + AssetProfile foundDefaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId); + Assert.assertNotNull(foundDefaultAssetProfile); + Assert.assertNotNull(foundDefaultAssetProfile.getId()); + Assert.assertNotNull(foundDefaultAssetProfile.getName()); + } + + @Test + public void testFindDefaultAssetProfileInfo() { + AssetProfileInfo foundDefaultAssetProfileInfo = assetProfileService.findDefaultAssetProfileInfo(tenantId); + Assert.assertNotNull(foundDefaultAssetProfileInfo); + Assert.assertNotNull(foundDefaultAssetProfileInfo.getId()); + Assert.assertNotNull(foundDefaultAssetProfileInfo.getName()); + } + + @Test + public void testFindOrCreateAssetProfile() throws ExecutionException, InterruptedException { + ListeningExecutorService testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + futures.add(testExecutor.submit(() -> assetProfileService.findOrCreateAssetProfile(tenantId, "Asset Profile 1"))); + futures.add(testExecutor.submit(() -> assetProfileService.findOrCreateAssetProfile(tenantId, "Asset Profile 2"))); + } + + List assetProfiles = Futures.allAsList(futures).get(); + assetProfiles.forEach(Assert::assertNotNull); + } finally { + testExecutor.shutdownNow(); + } + } + + @Test + public void testSetDefaultAssetProfile() { + AssetProfile assetProfile1 = this.createAssetProfile(tenantId, "Asset Profile 1"); + AssetProfile assetProfile2 = this.createAssetProfile(tenantId, "Asset Profile 2"); + + AssetProfile savedAssetProfile1 = assetProfileService.saveAssetProfile(assetProfile1); + AssetProfile savedAssetProfile2 = assetProfileService.saveAssetProfile(assetProfile2); + + boolean result = assetProfileService.setDefaultAssetProfile(tenantId, savedAssetProfile1.getId()); + Assert.assertTrue(result); + AssetProfile defaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId); + Assert.assertNotNull(defaultAssetProfile); + Assert.assertEquals(savedAssetProfile1.getId(), defaultAssetProfile.getId()); + result = assetProfileService.setDefaultAssetProfile(tenantId, savedAssetProfile2.getId()); + Assert.assertTrue(result); + defaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId); + Assert.assertNotNull(defaultAssetProfile); + Assert.assertEquals(savedAssetProfile2.getId(), defaultAssetProfile.getId()); + } + + @Test(expected = DataValidationException.class) + public void testSaveAssetProfileWithEmptyName() { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setTenantId(tenantId); + assetProfileService.saveAssetProfile(assetProfile); + } + + @Test(expected = DataValidationException.class) + public void testSaveAssetProfileWithSameName() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + assetProfileService.saveAssetProfile(assetProfile); + AssetProfile assetProfile2 = this.createAssetProfile(tenantId, "Asset Profile"); + assetProfileService.saveAssetProfile(assetProfile2); + } + + @Test(expected = DataValidationException.class) + public void testDeleteAssetProfileWithExistingAsset() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("Test asset"); + asset.setAssetProfileId(savedAssetProfile.getId()); + assetService.saveAsset(asset); + assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + } + + @Test + public void testDeleteAssetProfile() { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); + AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); + assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + AssetProfile foundAssetProfile = assetProfileService.findAssetProfileById(tenantId, savedAssetProfile.getId()); + Assert.assertNull(foundAssetProfile); + } + + @Test + public void testFindAssetProfiles() { + + List assetProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData pageData = assetProfileService.findAssetProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + assetProfiles.addAll(pageData.getData()); + + for (int i = 0; i < 28; i++) { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile" + i); + assetProfiles.add(assetProfileService.saveAssetProfile(assetProfile)); + } + + List loadedAssetProfiles = new ArrayList<>(); + pageLink = new PageLink(17); + do { + pageData = assetProfileService.findAssetProfiles(tenantId, pageLink); + loadedAssetProfiles.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + Collections.sort(assetProfiles, idComparator); + Collections.sort(loadedAssetProfiles, idComparator); + + Assert.assertEquals(assetProfiles, loadedAssetProfiles); + + for (AssetProfile assetProfile : loadedAssetProfiles) { + if (!assetProfile.isDefault()) { + assetProfileService.deleteAssetProfile(tenantId, assetProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = assetProfileService.findAssetProfiles(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + + @Test + public void testFindAssetProfileInfos() { + + List assetProfiles = new ArrayList<>(); + PageLink pageLink = new PageLink(17); + PageData assetProfilePageData = assetProfileService.findAssetProfiles(tenantId, pageLink); + Assert.assertFalse(assetProfilePageData.hasNext()); + Assert.assertEquals(1, assetProfilePageData.getTotalElements()); + assetProfiles.addAll(assetProfilePageData.getData()); + + for (int i = 0; i < 28; i++) { + AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile" + i); + assetProfiles.add(assetProfileService.saveAssetProfile(assetProfile)); + } + + List loadedAssetProfileInfos = new ArrayList<>(); + pageLink = new PageLink(17); + PageData pageData; + do { + pageData = assetProfileService.findAssetProfileInfos(tenantId, pageLink); + loadedAssetProfileInfos.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageLink.nextPageLink(); + } + } while (pageData.hasNext()); + + + Collections.sort(assetProfiles, idComparator); + Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator); + + List assetProfileInfos = assetProfiles.stream() + .map(assetProfile -> new AssetProfileInfo(assetProfile.getId(), + assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList()); + + Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos); + + for (AssetProfile assetProfile : assetProfiles) { + if (!assetProfile.isDefault()) { + assetProfileService.deleteAssetProfile(tenantId, assetProfile.getId()); + } + } + + pageLink = new PageLink(17); + pageData = assetProfileService.findAssetProfileInfos(tenantId, pageLink); + Assert.assertFalse(pageData.hasNext()); + Assert.assertEquals(1, pageData.getTotalElements()); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index 0a6cd0f33c..a5a7fcfaf0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -262,7 +262,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); asset.setType("default"); - assetsTitle1.add(new AssetInfo(assetService.saveAsset(asset), null, false)); + assetsTitle1.add(new AssetInfo(assetService.saveAsset(asset), null, false, "default")); } String title2 = "Asset title 2"; List assetsTitle2 = new ArrayList<>(); @@ -274,7 +274,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); asset.setType("default"); - assetsTitle2.add(new AssetInfo(assetService.saveAsset(asset), null, false)); + assetsTitle2.add(new AssetInfo(assetService.saveAsset(asset), null, false, "default")); } List loadedAssetsTitle1 = new ArrayList<>(); @@ -427,7 +427,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { asset.setName("Asset"+i); asset.setType("default"); asset = assetService.saveAsset(asset); - assets.add(new AssetInfo(assetService.assignAssetToCustomer(tenantId, asset.getId(), customerId), customer.getTitle(), customer.isPublic())); + assets.add(new AssetInfo(assetService.assignAssetToCustomer(tenantId, asset.getId(), customerId), customer.getTitle(), customer.isPublic(), "default")); } List loadedAssets = new ArrayList<>(); @@ -653,4 +653,158 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Assert.assertNull("Can't find asset by name in cache if it was renamed", renamedAsset); assetService.deleteAsset(tenantId, savedAsset.getId()); } + + @Test + public void testFindAssetInfoByTenantId() { + Customer customer = new Customer(); + customer.setTitle("Customer X"); + customer.setTenantId(tenantId); + Customer savedCustomer = customerService.saveCustomer(customer); + + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("default"); + asset.setType("default"); + asset.setLabel("label"); + asset.setCustomerId(savedCustomer.getId()); + + Asset savedAsset = assetService.saveAsset(asset); + + PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); + List assetInfosWithLabel = assetService + .findAssetInfosByTenantId(tenantId, pageLinkWithLabel).getData(); + + Assert.assertFalse(assetInfosWithLabel.isEmpty()); + Assert.assertTrue( + assetInfosWithLabel.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getLabel().equals(savedAsset.getLabel()) + ) + ); + + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + List assetInfosWithCustomer = assetService + .findAssetInfosByTenantId(tenantId, pageLinkWithCustomer).getData(); + + Assert.assertFalse(assetInfosWithCustomer.isEmpty()); + Assert.assertTrue( + assetInfosWithCustomer.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getCustomerId().equals(savedCustomer.getId()) + && d.getCustomerTitle().equals(savedCustomer.getTitle()) + ) + ); + + PageLink pageLinkWithType = new PageLink(100, 0, asset.getType()); + List assetInfosWithType = assetService + .findAssetInfosByTenantId(tenantId, pageLinkWithType).getData(); + + Assert.assertFalse(assetInfosWithType.isEmpty()); + Assert.assertTrue( + assetInfosWithType.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getType().equals(asset.getType()) + ) + ); + } + + @Test + public void testFindAssetInfoByTenantIdAndType() { + Customer customer = new Customer(); + customer.setTitle("Customer X"); + customer.setTenantId(tenantId); + Customer savedCustomer = customerService.saveCustomer(customer); + + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("default"); + asset.setType("default"); + asset.setLabel("label"); + asset.setCustomerId(savedCustomer.getId()); + Asset savedAsset = assetService.saveAsset(asset); + + PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); + List assetInfosWithLabel = assetService + .findAssetInfosByTenantIdAndType(tenantId, asset.getType(), pageLinkWithLabel).getData(); + + Assert.assertFalse(assetInfosWithLabel.isEmpty()); + Assert.assertTrue( + assetInfosWithLabel.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getAssetProfileName().equals(savedAsset.getType()) + && d.getLabel().equals(savedAsset.getLabel()) + ) + ); + + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + List assetInfosWithCustomer = assetService + .findAssetInfosByTenantIdAndType(tenantId, asset.getType(), pageLinkWithCustomer).getData(); + + Assert.assertFalse(assetInfosWithCustomer.isEmpty()); + Assert.assertTrue( + assetInfosWithCustomer.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getAssetProfileName().equals(savedAsset.getType()) + && d.getCustomerId().equals(savedCustomer.getId()) + && d.getCustomerTitle().equals(savedCustomer.getTitle()) + ) + ); + } + + @Test + public void testFindAssetInfoByTenantIdAndAssetProfileId() { + Customer customer = new Customer(); + customer.setTitle("Customer X"); + customer.setTenantId(tenantId); + Customer savedCustomer = customerService.saveCustomer(customer); + + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("default"); + asset.setLabel("label"); + asset.setCustomerId(savedCustomer.getId()); + Asset savedAsset = assetService.saveAsset(asset); + + PageLink pageLinkWithLabel = new PageLink(100, 0, "label"); + List assetInfosWithLabel = assetService + .findAssetInfosByTenantIdAndAssetProfileId(tenantId, savedAsset.getAssetProfileId(), pageLinkWithLabel).getData(); + + Assert.assertFalse(assetInfosWithLabel.isEmpty()); + Assert.assertTrue( + assetInfosWithLabel.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getAssetProfileId().equals(savedAsset.getAssetProfileId()) + && d.getLabel().equals(savedAsset.getLabel()) + ) + ); + + PageLink pageLinkWithCustomer = new PageLink(100, 0, savedCustomer.getSearchText()); + List assetInfosWithCustomer = assetService + .findAssetInfosByTenantIdAndAssetProfileId(tenantId, savedAsset.getAssetProfileId(), pageLinkWithCustomer).getData(); + + Assert.assertFalse(assetInfosWithCustomer.isEmpty()); + Assert.assertTrue( + assetInfosWithCustomer.stream() + .anyMatch( + d -> d.getId().equals(savedAsset.getId()) + && d.getTenantId().equals(tenantId) + && d.getAssetProfileId().equals(savedAsset.getAssetProfileId()) + && d.getCustomerId().equals(savedCustomer.getId()) + && d.getCustomerTitle().equals(savedCustomer.getTitle()) + ) + ); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetProfileServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetProfileServiceSqlTest.java new file mode 100644 index 0000000000..11c61e7c36 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/AssetProfileServiceSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.sql; + +import org.thingsboard.server.dao.service.BaseAssetProfileServiceTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AssetProfileServiceSqlTest extends BaseAssetProfileServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index 727f329229..7c3365d26e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java @@ -22,17 +22,23 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; 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.AbstractJpaDaoTest; import org.thingsboard.server.dao.asset.AssetDao; +import org.thingsboard.server.dao.asset.AssetProfileDao; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -57,6 +63,11 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { @Autowired private AssetDao assetDao; + @Autowired + private AssetProfileDao assetProfileDao; + + private Map savedAssetProfiles = new HashMap<>(); + @Before public void setUp() { tenantId1 = Uuids.timeBased(); @@ -67,7 +78,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { UUID assetId = Uuids.timeBased(); UUID tenantId = i % 2 == 0 ? tenantId1 : tenantId2; UUID customerId = i % 2 == 0 ? customerId1 : customerId2; - assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1")); + assets.add(saveAsset(assetId, tenantId, customerId, "ASSET_" + i)); } assertEquals(assets.size(), assetDao.find(TenantId.fromUUID(tenantId1)).size()); } @@ -78,6 +89,10 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { assetDao.removeById(asset.getTenantId(), asset.getUuidId()); } assets.clear(); + for (AssetProfileId assetProfileId : savedAssetProfiles.values()) { + assetProfileDao.removeById(TenantId.SYS_TENANT_ID, assetProfileId.getId()); + } + savedAssetProfiles.clear(); } @Test @@ -146,7 +161,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { public void testFindAssetsByTenantIdAndName() { UUID assetId = Uuids.timeBased(); String name = "TEST_ASSET"; - assets.add(saveAsset(assetId, tenantId2, customerId2, name, "TYPE_1")); + assets.add(saveAsset(assetId, tenantId2, customerId2, name)); Optional assetOpt1 = assetDao.findAssetsByTenantIdAndName(tenantId2, name); assertTrue("Optional expected to be non-empty", assetOpt1.isPresent()); @@ -197,7 +212,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { List tenant2Types = assetDao.findTenantAssetTypesAsync(tenantId2).get(30, TimeUnit.SECONDS); assertNotNull(tenant2Types); - List types = List.of("TYPE_1", "TYPE_2", "TYPE_3", "TYPE_4"); + List types = List.of("default", "TYPE_1", "TYPE_2", "TYPE_3", "TYPE_4"); assertEquals(getDifferentTypesCount(types, tenant1Types), tenant1Types.size()); assertEquals(getDifferentTypesCount(types, tenant2Types), tenant2Types.size()); } @@ -206,13 +221,36 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { return foundedAssetsTypes.stream().filter(type -> types.contains(type.getType())).count(); } + private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name) { + return saveAsset(id, tenantId, customerId, name, null); + } + private Asset saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) { + if (type == null) { + type = "default"; + } Asset asset = new Asset(); asset.setId(new AssetId(id)); asset.setTenantId(TenantId.fromUUID(tenantId)); asset.setCustomerId(new CustomerId(customerId)); asset.setName(name); asset.setType(type); + asset.setAssetProfileId(assetProfileId(type)); return assetDao.save(TenantId.fromUUID(tenantId), asset); } + + private AssetProfileId assetProfileId(String type) { + AssetProfileId assetProfileId = savedAssetProfiles.get(type); + if (assetProfileId == null) { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setName(type); + assetProfile.setTenantId(TenantId.SYS_TENANT_ID); + assetProfile.setDescription("Test"); + AssetProfile savedAssetProfile = assetProfileDao.save(TenantId.SYS_TENANT_ID, assetProfile); + assetProfileId = savedAssetProfile.getId(); + savedAssetProfiles.put(type, assetProfileId); + } + return assetProfileId; + } + } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 27f82f331c..15131011d8 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -48,6 +48,9 @@ cache.specs.tenantProfiles.maxSize=100000 cache.specs.deviceProfiles.timeToLiveInMinutes=1440 cache.specs.deviceProfiles.maxSize=100000 +cache.specs.assetProfiles.timeToLiveInMinutes=1440 +cache.specs.assetProfiles.maxSize=100000 + cache.specs.attributes.timeToLiveInMinutes=1440 cache.specs.attributes.maxSize=100000 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 3a6d92a903..2116bc367d 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -21,6 +21,7 @@ DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS asset_profile; DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS rule_node_state; DROP TABLE IF EXISTS rule_node; diff --git a/dao/src/test/resources/sql/system-test-psql.sql b/dao/src/test/resources/sql/system-test-psql.sql index 177f838bb1..8d3f08a32f 100644 --- a/dao/src/test/resources/sql/system-test-psql.sql +++ b/dao/src/test/resources/sql/system-test-psql.sql @@ -1,2 +1,2 @@ --PostgreSQL specific truncate to fit constraints -TRUNCATE TABLE device_credentials, device, device_profile, ota_package, rule_node_state, rule_node, rule_chain; \ No newline at end of file +TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain; \ No newline at end of file diff --git a/dao/src/test/resources/sql/system-test.sql b/dao/src/test/resources/sql/system-test.sql index d673548fb0..6660dd52d6 100644 --- a/dao/src/test/resources/sql/system-test.sql +++ b/dao/src/test/resources/sql/system-test.sql @@ -1,6 +1,8 @@ TRUNCATE TABLE device_credentials; TRUNCATE TABLE device; +TRUNCATE TABLE asset; TRUNCATE TABLE device_profile CASCADE; +TRUNCATE TABLE asset_profile CASCADE; TRUNCATE TABLE rule_node_state; TRUNCATE TABLE rule_node; TRUNCATE TABLE rule_chain; \ No newline at end of file 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 735192374d..80330a5ef5 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -24,6 +24,7 @@ DROP TABLE IF EXISTS rule_chain; DROP TABLE IF EXISTS entity_view; DROP TABLE IF EXISTS device_profile; DROP TABLE IF EXISTS tenant_profile; +DROP TABLE IF EXISTS asset_profile; DROP TABLE IF EXISTS dashboard; DROP TABLE IF EXISTS edge; DROP TABLE IF EXISTS edge_event; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAssetProfileCache.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAssetProfileCache.java new file mode 100644 index 0000000000..00346c75e4 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAssetProfileCache.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api; + +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Created by ashvayka on 02.04.18. + */ +public interface RuleEngineAssetProfileCache { + + AssetProfile get(TenantId tenantId, AssetProfileId assetProfileId); + + AssetProfile get(TenantId tenantId, AssetId assetId); + + void addListener(TenantId tenantId, EntityId listenerId, Consumer profileListener, BiConsumer assetlistener); + + void removeListener(TenantId tenantId, EntityId listenerId); + +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1dac01f788..2feaf3fd49 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -234,6 +234,8 @@ public interface TbContext { RuleEngineDeviceProfileCache getDeviceProfileCache(); + RuleEngineAssetProfileCache getAssetProfileCache(); + EdgeService getEdgeService(); EdgeEventService getEdgeEventService(); diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java index c84020b6b5..de26761503 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java @@ -43,6 +43,7 @@ public class RelatedEntitiesParser { Map.entry("COPY public.widget_type ", EntityType.WIDGET_TYPE), Map.entry("COPY public.tenant_profile ", EntityType.TENANT_PROFILE), Map.entry("COPY public.device_profile ", EntityType.DEVICE_PROFILE), + Map.entry("COPY public.asset_profile ", EntityType.ASSET_PROFILE), Map.entry("COPY public.api_usage_state ", EntityType.API_USAGE_STATE) );