diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.cql b/application/src/main/data/upgrade/2.1.1/schema_update.cql new file mode 100644 index 0000000000..c477e8a02a --- /dev/null +++ b/application/src/main/data/upgrade/2.1.1/schema_update.cql @@ -0,0 +1,81 @@ +-- +-- Copyright © 2016-2018 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 MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name; +DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text; +DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer; +DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_entity_id; + +DROP TABLE IF EXISTS thingsboard.entity_views; +ControllerSqlTestSuite +CREATE TABLE IF NOT EXISTS thingsboard.entity_views ( + id timeuuid, + entity_id timeuuid, + entity_type text, + tenant_id timeuuid, + customer_id timeuuid, + name text, + keys text, + start_ts bigint, + end_ts bigint, + search_text text, + additional_info text, + PRIMARY KEY (id, entity_id, tenant_id, customer_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND entity_id IS NOT NULL + AND customer_id IS NOT NULL + AND name IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, name, id, customer_id, entity_id) + WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND entity_id IS NOT NULL + AND customer_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id) + WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND entity_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id) + WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND entity_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id) + WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); \ No newline at end of file diff --git a/application/src/main/data/upgrade/2.1.1/schema_update.sql b/application/src/main/data/upgrade/2.1.1/schema_update.sql new file mode 100644 index 0000000000..bd2c341e7f --- /dev/null +++ b/application/src/main/data/upgrade/2.1.1/schema_update.sql @@ -0,0 +1,31 @@ +-- +-- Copyright © 2016-2018 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 TABLE IF EXISTS entity_views; + +CREATE TABLE IF NOT EXISTS entity_views ( + id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, + entity_id varchar(31), + entity_type varchar(255), + tenant_id varchar(31), + customer_id varchar(31), + name varchar(255), + keys varchar(255), + start_ts bigint, + end_ts bigint, + search_text varchar(255), + additional_info varchar +); 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 2f78672800..8930f8476e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -48,6 +48,7 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -159,6 +160,10 @@ public class ActorSystemContext { @Getter private AuditLogService auditLogService; + @Autowired + @Getter + private EntityViewService entityViewService; + @Autowired @Getter private TelemetrySubscriptionService tsSubService; 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 d5260da10b..4e779d452b 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 @@ -41,6 +41,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -212,6 +213,11 @@ class DefaultTbContext implements TbContext { return mainCtx.getRelationService(); } + @Override + public EntityViewService getEntityViewService() { + return mainCtx.getEntityViewService(); + } + @Override public MailService getMailService() { if (mainCtx.isAllowSystemMailService()) { 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 de73fe0796..889630b81f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -56,6 +56,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; @@ -139,6 +140,9 @@ public abstract class BaseController { @Autowired protected DeviceStateService deviceStateService; + @Autowired + protected EntityViewService entityViewService; + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { errorResponseHandler.handle(ex, response); @@ -313,6 +317,9 @@ public abstract class BaseController { case USER: checkUserId(new UserId(entityId.getId())); return; + case ENTITY_VIEW: + checkEntityViewId(new EntityViewId(entityId.getId())); + return; default: throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); } @@ -340,6 +347,25 @@ public abstract class BaseController { } } + protected EntityView checkEntityViewId(EntityViewId entityViewId) throws ThingsboardException { + try { + validateId(entityViewId, "Incorrect entityViewId " + entityViewId); + EntityView entityView = entityViewService.findEntityViewById(entityViewId); + checkEntityView(entityView); + return entityView; + } catch (Exception e) { + throw handleException(e, false); + } + } + + protected void checkEntityView(EntityView entityView) throws ThingsboardException { + checkNotNull(entityView); + checkTenantId(entityView.getTenantId()); + if (entityView.getCustomerId() != null && !entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + checkCustomerId(entityView.getCustomerId()); + } + } + Asset checkAssetId(AssetId assetId) throws ThingsboardException { try { validateId(assetId, "Incorrect assetId " + assetId); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java new file mode 100644 index 0000000000..0ba35e85ad --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -0,0 +1,218 @@ +/** + * Copyright © 2016-2018 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 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.Customer; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; + +/** + * Created by Victor Basanets on 8/28/2017. + */ +@RestController +@RequestMapping("/api") +public class EntityViewController extends BaseController { + + public static final String ENTITY_VIEW_ID = "entityViewId"; + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET) + @ResponseBody + public EntityView getEntityViewById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId))); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entityView", method = RequestMethod.POST) + @ResponseBody + public EntityView saveEntityView(@RequestBody EntityView entityView) throws ThingsboardException { + try { + entityView.setTenantId(getCurrentUser().getTenantId()); + EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); + logEntityAction(savedEntityView.getId(), savedEntityView, null, + entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), entityView, null, + entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public void deleteEntityView(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + EntityView entityView = checkEntityViewId(entityViewId); + entityViewService.deleteEntityView(entityViewId); + logEntityAction(entityViewId, entityView, entityView.getCustomerId(), + ActionType.DELETED,null, strEntityViewId); + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), + null, + null, + ActionType.DELETED, e, strEntityViewId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST) + @ResponseBody + public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId, + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(CUSTOMER_ID, strCustomerId); + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId); + + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + checkEntityViewId(entityViewId); + + EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(entityViewId, customerId)); + logEntityAction(entityViewId, savedEntityView, + savedEntityView.getCustomerId(), + ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, strCustomerId, customer.getName()); + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strEntityViewId, strCustomerId); + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE) + @ResponseBody + public EntityView unassignEntityViewFromCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + checkParameter(ENTITY_VIEW_ID, strEntityViewId); + try { + EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); + EntityView entityView = checkEntityViewId(entityViewId); + if (entityView.getCustomerId() == null || entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + throw new IncorrectParameterException("Entity View isn't assigned to any customer!"); + } + Customer customer = checkCustomerId(entityView.getCustomerId()); + EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(entityViewId)); + logEntityAction(entityViewId, entityView, + entityView.getCustomerId(), + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEntityViewId, customer.getId().toString(), customer.getName()); + + return savedEntityView; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.ENTITY_VIEW), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEntityViewId); + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TextPageData getCustomerEntityViews( + @PathVariable("customerId") String strCustomerId, + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + checkParameter("customerId", strCustomerId); + try { + TenantId tenantId = getCurrentUser().getTenantId(); + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + checkCustomerId(customerId); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(entityViewService.findEntityViewsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/entityViews", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TextPageData getTenantEntityViews( + @RequestParam int limit, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String idOffset, + @RequestParam(required = false) String textOffset) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); + return checkNotNull(entityViewService.findEntityViewByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/entityViews", method = RequestMethod.POST) + @ResponseBody + public List findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException { + checkNotNull(query); + checkNotNull(query.getParameters()); + checkEntityId(query.getParameters().getEntityId()); + try { + List entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(query).get()); + entityViews = entityViews.stream().filter(entityView -> { + try { + checkEntityView(entityView); + return true; + } catch (ThingsboardException e) { + return false; + } + }).collect(Collectors.toList()); + return entityViews; + } catch (Exception e) { + throw handleException(e); + } + } +} 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 f863d0bb2a..da154e9906 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -24,9 +24,10 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.install.DataUpdateService; -import org.thingsboard.server.service.install.DatabaseSchemaService; import org.thingsboard.server.service.install.DatabaseUpgradeService; +import org.thingsboard.server.service.install.EntityDatabaseSchemaService; import org.thingsboard.server.service.install.SystemDataLoaderService; +import org.thingsboard.server.service.install.TsDatabaseSchemaService; @Service @Profile("install") @@ -43,7 +44,10 @@ public class ThingsboardInstallService { private Boolean loadDemo; @Autowired - private DatabaseSchemaService databaseSchemaService; + private EntityDatabaseSchemaService entityDatabaseSchemaService; + + @Autowired + private TsDatabaseSchemaService tsDatabaseSchemaService; @Autowired private DatabaseUpgradeService databaseUpgradeService; @@ -88,6 +92,11 @@ public class ThingsboardInstallService { dataUpdateService.updateData("1.4.0"); + case "2.0.0": + log.info("Upgrading ThingsBoard from version 2.0.0 to 2.1.1 ..."); + + databaseUpgradeService.upgradeDatabase("2.0.0"); + log.info("Updating system data..."); systemDataLoaderService.deleteSystemWidgetBundle("charts"); @@ -114,9 +123,13 @@ public class ThingsboardInstallService { log.info("Starting ThingsBoard Installation..."); - log.info("Installing DataBase schema..."); + log.info("Installing DataBase schema for entities..."); + + entityDatabaseSchemaService.createDatabaseSchema(); + + log.info("Installing DataBase schema for timeseries..."); - databaseSchemaService.createDatabaseSchema(); + tsDatabaseSchemaService.createDatabaseSchema(); log.info("Loading system data..."); diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java similarity index 78% rename from application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java rename to application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java index dd76b21844..10559ba2d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraAbstractDatabaseSchemaService.java @@ -17,24 +17,17 @@ package org.thingsboard.server.service.install; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; import org.thingsboard.server.dao.cassandra.CassandraInstallCluster; -import org.thingsboard.server.dao.util.NoSqlDao; import org.thingsboard.server.service.install.cql.CQLStatementsParser; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -@Service -@NoSqlDao -@Profile("install") @Slf4j -public class CassandraDatabaseSchemaService implements DatabaseSchemaService { +public abstract class CassandraAbstractDatabaseSchemaService implements DatabaseSchemaService { private static final String CASSANDRA_DIR = "cassandra"; - private static final String SCHEMA_CQL = "schema.cql"; @Autowired private CassandraInstallCluster cluster; @@ -42,10 +35,16 @@ public class CassandraDatabaseSchemaService implements DatabaseSchemaService { @Autowired private InstallScripts installScripts; + private final String schemaCql; + + protected CassandraAbstractDatabaseSchemaService(String schemaCql) { + this.schemaCql = schemaCql; + } + @Override public void createDatabaseSchema() throws Exception { - log.info("Installing Cassandra DataBase schema..."); - Path schemaFile = Paths.get(installScripts.getDataDir(), CASSANDRA_DIR, SCHEMA_CQL); + log.info("Installing Cassandra DataBase schema part: " + schemaCql); + Path schemaFile = Paths.get(installScripts.getDataDir(), CASSANDRA_DIR, schemaCql); loadCql(schemaFile); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java new file mode 100644 index 0000000000..b492a51cbc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraEntityDatabaseSchemaService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2018 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.NoSqlDao; + +@Service +@NoSqlDao +@Profile("install") +public class CassandraEntityDatabaseSchemaService extends CassandraAbstractDatabaseSchemaService + implements EntityDatabaseSchemaService { + public CassandraEntityDatabaseSchemaService() { + super("schema-entities.cql"); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java new file mode 100644 index 0000000000..1a3a2e429b --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseSchemaService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2018 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.NoSqlTsDao; + +@Service +@NoSqlTsDao +@Profile("install") +public class CassandraTsDatabaseSchemaService extends CassandraAbstractDatabaseSchemaService + implements TsDatabaseSchemaService { + public CassandraTsDatabaseSchemaService() { + super("schema-ts.cql"); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java new file mode 100644 index 0000000000..c215ea0d8f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/EntityDatabaseSchemaService.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2018 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.install; + +public interface EntityDatabaseSchemaService extends DatabaseSchemaService { +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java rename to application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 1daf66086a..4a39b8f60b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -18,9 +18,6 @@ package org.thingsboard.server.service.install; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.thingsboard.server.dao.util.SqlDao; import java.nio.charset.Charset; import java.nio.file.Files; @@ -29,14 +26,10 @@ import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; -@Service -@Profile("install") @Slf4j -@SqlDao -public class SqlDatabaseSchemaService implements DatabaseSchemaService { +public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService { private static final String SQL_DIR = "sql"; - private static final String SCHEMA_SQL = "schema.sql"; @Value("${spring.datasource.url}") private String dbUrl; @@ -50,12 +43,18 @@ public class SqlDatabaseSchemaService implements DatabaseSchemaService { @Autowired private InstallScripts installScripts; + private final String schemaSql; + + protected SqlAbstractDatabaseSchemaService(String schemaSql) { + this.schemaSql = schemaSql; + } + @Override public void createDatabaseSchema() throws Exception { - log.info("Installing SQL DataBase schema..."); + log.info("Installing SQL DataBase schema part: " + schemaSql); - Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, SCHEMA_SQL); + Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaSql); try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8")); conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema 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 3a4a837217..7d701b7a53 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 @@ -107,6 +107,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Schema updated."); } break; + case "2.0.0": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.1.1", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Schema updated."); + } + break; + default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java new file mode 100644 index 0000000000..bfd371b28a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2018 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.SqlDao; + +@Service +@SqlDao +@Profile("install") +public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService + implements EntityDatabaseSchemaService { + public SqlEntityDatabaseSchemaService() { + super("schema-entities.sql"); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java new file mode 100644 index 0000000000..03a35178bf --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseSchemaService.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2018 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.install; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.util.SqlTsDao; + +@Service +@SqlTsDao +@Profile("install") +public class SqlTsDatabaseSchemaService extends SqlAbstractDatabaseSchemaService + implements TsDatabaseSchemaService { + public SqlTsDatabaseSchemaService() { + super("schema-ts.sql"); + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java new file mode 100644 index 0000000000..0a2ba7586c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/TsDatabaseSchemaService.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2018 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.install; + +public interface TsDatabaseSchemaService extends DatabaseSchemaService { +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 600820ec73..eaf1018ff2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -26,6 +26,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -44,6 +46,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; @@ -66,6 +69,7 @@ public class AccessValidator { public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; + public static final String ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND = "Entity-view with requested id wasn't found!"; @Autowired protected TenantService tenantService; @@ -88,6 +92,9 @@ public class AccessValidator { @Autowired protected RuleChainService ruleChainService; + @Autowired + protected EntityViewService entityViewService; + private ExecutorService executor; @PostConstruct @@ -158,6 +165,9 @@ public class AccessValidator { case TENANT: validateTenant(currentUser, entityId, callback); return; + case ENTITY_VIEW: + validateEntityView(currentUser, entityId, callback); + return; default: //TODO: add support of other entities throw new IllegalStateException("Not Implemented!"); @@ -293,6 +303,27 @@ public class AccessValidator { } } + private void validateEntityView(final SecurityUser currentUser, EntityId entityId, FutureCallback callback) { + if (currentUser.isSystemAdmin()) { + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); + } else { + ListenableFuture entityViewFuture = entityViewService.findEntityViewByIdAsync(new EntityViewId(entityId.getId())); + Futures.addCallback(entityViewFuture, getCallback(callback, entityView -> { + if (entityView == null) { + return ValidationResult.entityNotFound(ENTITY_VIEW_WITH_REQUESTED_ID_NOT_FOUND); + } else { + if (!entityView.getTenantId().equals(currentUser.getTenantId())) { + return ValidationResult.accessDenied("Entity-view doesn't belong to the current Tenant!"); + } else if (currentUser.isCustomerUser() && !entityView.getCustomerId().equals(currentUser.getCustomerId())) { + return ValidationResult.accessDenied("Entity-view doesn't belong to the current Customer!"); + } else { + return ValidationResult.ok(entityView); + } + } + }), executor); + } + } + private FutureCallback getCallback(FutureCallback callback, Function> transformer) { return new FutureCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 5c3b88a253..bcca43ca5e 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -29,9 +29,11 @@ import org.thingsboard.rule.engine.api.util.DonAsynchron; import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -48,6 +50,8 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.cluster.SendToClusterMsg; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.cluster.ClusterAPIProtos; import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; @@ -64,6 +68,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -101,6 +106,9 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @Autowired private ClusterRpcService rpcService; + @Autowired + private EntityViewService entityViewService; + @Autowired @Lazy private DeviceStateService stateService; @@ -133,20 +141,64 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio @Override public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) { + long startTime = 0L; + long endTime = 0L; + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { + EntityView entityView = entityViewService.findEntityViewById(new EntityViewId(entityId.getId())); + entityId = entityView.getEntityId(); + startTime = entityView.getStartTimeMs(); + endTime = entityView.getEndTimeMs(); + sub = getUpdatedSubscriptionState(entityId, sub, entityView); + } Optional server = routingService.resolveById(entityId); Subscription subscription; if (server.isPresent()) { ServerAddress address = server.get(); - log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address); - subscription = new Subscription(sub, true, address); + log.trace("[{}] Forwarding subscription [{}] for [{}] entity [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId.getEntityType().name(), entityId, address); + subscription = new Subscription(sub, true, address, startTime, endTime); tellNewSubscription(address, sessionId, subscription); } else { - log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId); - subscription = new Subscription(sub, true); + log.trace("[{}] Registering local subscription [{}] for [{}] entity [{}]", sessionId, sub.getSubscriptionId(), entityId.getEntityType().name(), entityId); + subscription = new Subscription(sub, true, null, startTime, endTime); } registerSubscription(sessionId, entityId, subscription); } + private SubscriptionState getUpdatedSubscriptionState(EntityId entityId, SubscriptionState sub, EntityView entityView) { + boolean allKeys; + Map keyStates; + if (sub.getType().equals(TelemetryFeature.TIMESERIES) && !entityView.getKeys().getTimeseries().isEmpty()) { + allKeys = false; + keyStates = sub.getKeyStates().entrySet() + .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } else if (sub.getType().equals(TelemetryFeature.ATTRIBUTES)) { + if (sub.getScope().equals(DataConstants.CLIENT_SCOPE) && !entityView.getKeys().getAttributes().getCs().isEmpty()) { + allKeys = false; + keyStates = filterMap(sub, entityView.getKeys().getAttributes().getCs()); + } else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSs().isEmpty()) { + allKeys = false; + keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSs()); + } else if (sub.getScope().equals(DataConstants.SERVER_SCOPE) && !entityView.getKeys().getAttributes().getSh().isEmpty()) { + allKeys = false; + keyStates = filterMap(sub, entityView.getKeys().getAttributes().getSh()); + } else { + allKeys = sub.isAllKeys(); + keyStates = sub.getKeyStates(); + } + } else { + allKeys = sub.isAllKeys(); + keyStates = sub.getKeyStates(); + } + return new SubscriptionState(sub.getWsSessionId(), sub.getSubscriptionId(), entityId, sub.getType(), allKeys, keyStates, sub.getScope()); + } + + private Map filterMap(SubscriptionState sub, List allowedKeys) { + return sub.getKeyStates().entrySet() + .stream().filter(entry -> allowedKeys.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + @Override public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) { cleanupLocalWsSessionSubscriptions(sessionId); @@ -415,7 +467,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> { List subscriptionUpdate = null; for (AttributeKvEntry kv : attributes) { - if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) { + if (isInTimeRange(s, kv.getLastUpdateTs()) && (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey()))) { if (subscriptionUpdate == null) { subscriptionUpdate = new ArrayList<>(); } @@ -430,7 +482,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> { List subscriptionUpdate = null; for (TsKvEntry kv : ts) { - if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) { + if (isInTimeRange(s, kv.getTs()) && (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey())))) { if (subscriptionUpdate == null) { subscriptionUpdate = new ArrayList<>(); } @@ -441,6 +493,11 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio }); } + private boolean isInTimeRange(Subscription subscription, long kvTime) { + return (subscription.getStartTime() == 0 || subscription.getStartTime() <= kvTime) + && (subscription.getEndTime() == 0 || subscription.getEndTime() >= kvTime); + } + private void onLocalSubUpdate(EntityId entityId, Predicate filter, Function> f) { Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); if (deviceSubscriptions != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java index 811c055372..32d6243a61 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/Subscription.java @@ -30,9 +30,11 @@ public class Subscription { private final SubscriptionState sub; private final boolean local; private ServerAddress server; + private long startTime; + private long endTime; - public Subscription(SubscriptionState sub, boolean local) { - this(sub, local, null); + public Subscription(SubscriptionState sub, boolean local, ServerAddress server) { + this(sub, local, server, 0L, 0L); } public String getWsSessionId() { diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 052490939a..720bb9f490 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -159,7 +159,11 @@ quota: intervalMin: 2 database: - type: "${DATABASE_TYPE:sql}" # cassandra OR sql + entities: + type: "${DATABASE_ENTITIES_TYPE:sql}" # cassandra OR sql + ts: + type: "${DATABASE_TS_TYPE:sql}" # cassandra OR sql (for hybrid mode, only this value should be cassandra) + # Cassandra driver configuration parameters cassandra: @@ -206,7 +210,7 @@ cassandra: write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}" default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS,INDEFINITE - ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" + ts_key_value_partitioning: "${TS_KV_PARTITIONING:INDEFINITE}" ts_key_value_ttl: "${TS_KV_TTL:0}" buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}" concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}" @@ -289,6 +293,9 @@ caffeine: assets: timeToLiveInMinutes: 1440 maxSize: 100000 + entityViews: + timeToLiveInMinutes: 1440 + maxSize: 100000 redis: # standalone or cluster @@ -350,7 +357,7 @@ spring: password: "${SPRING_DATASOURCE_PASSWORD:}" # PostgreSQL DAO Configuration -#spring: +# spring: # data: # sql: # repositories: diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java new file mode 100644 index 0000000000..ee97b97fa0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -0,0 +1,456 @@ +/** + * Copyright © 2016-2018 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.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.objects.AttributesEntityView; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; + +public abstract class BaseEntityViewControllerTest extends AbstractControllerTest { + + private IdComparator idComparator; + private Tenant savedTenant; + private User tenantAdmin; + private Device testDevice; + private TelemetryEntityView telemetry; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + idComparator = new IdComparator<>(); + + savedTenant = doPost("/api/tenant", getNewTenant("My 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"); + + Device device = new Device(); + device.setName("Test device"); + device.setType("default"); + testDevice = doPost("/api/device", device, Device.class); + + telemetry = new TelemetryEntityView( + Arrays.asList("109", "209", "309"), + new AttributesEntityView( + Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4"), + Arrays.asList("saValue1", "saValue2", "saValue3", "saValue4"), + Arrays.asList("shValue1", "shValue2", "shValue3", "shValue4"))); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testFindEntityViewById() throws Exception { + EntityView savedView = getNewSavedEntityView("Test entity view"); + EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + Assert.assertNotNull(foundView); + assertEquals(savedView, foundView); + } + + @Test + public void testSaveEntityView() throws Exception { + EntityView savedView = getNewSavedEntityView("Test entity view"); + + Assert.assertNotNull(savedView); + Assert.assertNotNull(savedView.getId()); + Assert.assertTrue(savedView.getCreatedTime() > 0); + assertEquals(savedTenant.getId(), savedView.getTenantId()); + Assert.assertNotNull(savedView.getCustomerId()); + assertEquals(NULL_UUID, savedView.getCustomerId().getId()); + assertEquals(savedView.getName(), savedView.getName()); + + savedView.setName("New test entity view"); + doPost("/api/entityView", savedView, EntityView.class); + EntityView foundEntityView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + + assertEquals(foundEntityView.getName(), savedView.getName()); + assertEquals(foundEntityView.getKeys(), telemetry); + } + + @Test + public void testDeleteEntityView() throws Exception { + EntityView view = getNewSavedEntityView("Test entity view"); + Customer customer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class); + view.setCustomerId(customer.getId()); + EntityView savedView = doPost("/api/entityView", view, EntityView.class); + + doDelete("/api/entityView/" + savedView.getId().getId().toString()) + .andExpect(status().isOk()); + + doGet("/api/entityView/" + savedView.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testSaveEntityViewWithEmptyName() throws Exception { + doPost("/api/entityView", new EntityView()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("Entity view name should be specified!"))); + } + + @Test + public void testAssignAndUnAssignedEntityViewToCustomer() throws Exception { + EntityView view = getNewSavedEntityView("Test entity view"); + Customer savedCustomer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class); + view.setCustomerId(savedCustomer.getId()); + EntityView savedView = doPost("/api/entityView", view, EntityView.class); + + EntityView assignedView = doPost( + "/api/customer/" + savedCustomer.getId().getId().toString() + "/entityView/" + savedView.getId().getId().toString(), + EntityView.class); + assertEquals(savedCustomer.getId(), assignedView.getCustomerId()); + + EntityView foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(savedCustomer.getId(), foundView.getCustomerId()); + + EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId()); + + foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class); + assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId()); + } + + @Test + public void testAssignEntityViewToNonExistentCustomer() throws Exception { + EntityView savedView = getNewSavedEntityView("Test entity view"); + doPost("/api/customer/" + UUIDs.timeBased().toString() + "/device/" + savedView.getId().getId().toString()) + .andExpect(status().isNotFound()); + } + + @Test + public void testAssignEntityViewToCustomerFromDifferentTenant() throws Exception { + loginSysAdmin(); + + Tenant tenant2 = getNewTenant("Different tenant"); + Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class); + Assert.assertNotNull(savedTenant2); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(savedTenant2.getId()); + tenantAdmin2.setEmail("tenant3@thingsboard.org"); + tenantAdmin2.setFirstName("Joe"); + tenantAdmin2.setLastName("Downs"); + createUserAndLogin(tenantAdmin2, "testPassword1"); + + Customer customer = getNewCustomer("Different customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + login(tenantAdmin.getEmail(), "testPassword1"); + + EntityView savedView = getNewSavedEntityView("Test entity view"); + + doPost("/api/customer/" + savedCustomer.getId().getId().toString() + "/entityView/" + savedView.getId().getId().toString()) + .andExpect(status().isForbidden()); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant2.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testGetCustomerEntityViews() throws Exception { + CustomerId customerId = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class).getId(); + String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViews?"; + + List views = new ArrayList<>(); + for (int i = 0; i < 128; i++) { + views.add(doPost("/api/customer/" + customerId.getId().toString() + "/entityView/" + + getNewSavedEntityView("Test entity view " + i).getId().getId().toString(), EntityView.class)); + } + + List loadedViews = loadListOf(new TextPageLink(23), urlTemplate); + + Collections.sort(views, idComparator); + Collections.sort(loadedViews, idComparator); + + assertEquals(views, loadedViews); + } + + @Test + public void testGetCustomerEntityViewsByName() throws Exception { + CustomerId customerId = doPost("/api/customer", getNewCustomer("Test customer"), Customer.class).getId(); + String urlTemplate = "/api/customer/" + customerId.getId().toString() + "/entityViews?"; + + String name1 = "Entity view name1"; + List namesOfView1 = fillListOf(125, name1, "/api/customer/" + customerId.getId().toString() + + "/entityView/"); + List loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), urlTemplate); + Collections.sort(namesOfView1, idComparator); + Collections.sort(loadedNamesOfView1, idComparator); + assertEquals(namesOfView1, loadedNamesOfView1); + + String name2 = "Entity view name2"; + List NamesOfView2 = fillListOf(143, name2, "/api/customer/" + customerId.getId().toString() + + "/entityView/"); + List loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), urlTemplate); + Collections.sort(NamesOfView2, idComparator); + Collections.sort(loadedNamesOfView2, idComparator); + assertEquals(NamesOfView2, loadedNamesOfView2); + + for (EntityView view : loadedNamesOfView1) { + doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); + } + TextPageData pageData = doGetTypedWithPageLink(urlTemplate, + new TypeReference>() { + }, new TextPageLink(4, name1)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + + for (EntityView view : loadedNamesOfView2) { + doDelete("/api/customer/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); + } + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + }, + new TextPageLink(4, name2)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + } + + @Test + public void testGetTenantEntityViews() throws Exception { + + List views = new ArrayList<>(); + for (int i = 0; i < 178; i++) { + views.add(getNewSavedEntityView("Test entity view" + i)); + } + List loadedViews = loadListOf(new TextPageLink(23), "/api/tenant/entityViews?"); + + Collections.sort(views, idComparator); + Collections.sort(loadedViews, idComparator); + + assertEquals(views, loadedViews); + } + + @Test + public void testGetTenantEntityViewsByName() throws Exception { + String name1 = "Entity view name1"; + List namesOfView1 = fillListOf(143, name1); + List loadedNamesOfView1 = loadListOf(new TextPageLink(15, name1), "/api/tenant/entityViews?"); + Collections.sort(namesOfView1, idComparator); + Collections.sort(loadedNamesOfView1, idComparator); + assertEquals(namesOfView1, loadedNamesOfView1); + + String name2 = "Entity view name2"; + List NamesOfView2 = fillListOf(75, name2); + List loadedNamesOfView2 = loadListOf(new TextPageLink(4, name2), "/api/tenant/entityViews?"); + Collections.sort(NamesOfView2, idComparator); + Collections.sort(loadedNamesOfView2, idComparator); + assertEquals(NamesOfView2, loadedNamesOfView2); + + for (EntityView view : loadedNamesOfView1) { + doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); + } + TextPageData pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", + new TypeReference>() { + }, new TextPageLink(4, name1)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + + for (EntityView view : loadedNamesOfView2) { + doDelete("/api/entityView/" + view.getId().getId().toString()).andExpect(status().isOk()); + } + pageData = doGetTypedWithPageLink("/api/tenant/entityViews?", new TypeReference>() { + }, + new TextPageLink(4, name2)); + Assert.assertFalse(pageData.hasNext()); + assertEquals(0, pageData.getData().size()); + } + + @Test + public void testTheCopyOfAttrsIntoTSForTheView() throws Exception { + Set actualAttributesSet = + getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}"); + + Set expectedActualAttributesSet = + new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4")); + assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet)); + Thread.sleep(1000); + + EntityView savedView = getNewSavedEntityView("Test entity view"); + List> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + + assertEquals("value1", getValue(values, "caValue1")); + assertEquals(true, getValue(values, "caValue2")); + assertEquals(42.0, getValue(values, "caValue3")); + assertEquals(73, getValue(values, "caValue4")); + } + + @Test + public void testTheCopyOfAttrsOutOfTSForTheView() throws Exception { + Set actualAttributesSet = + getAttributesByKeys("{\"caValue1\":\"value1\", \"caValue2\":true, \"caValue3\":42.0, \"caValue4\":73}"); + + Set expectedActualAttributesSet = new HashSet<>(Arrays.asList("caValue1", "caValue2", "caValue3", "caValue4")); + assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet)); + Thread.sleep(1000); + + List> valueTelemetryOfDevices = doGetAsync("/api/plugins/telemetry/DEVICE/" + testDevice.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + + EntityView view = new EntityView(); + view.setEntityId(testDevice.getId()); + view.setTenantId(savedTenant.getId()); + view.setName("Test entity view"); + view.setKeys(telemetry); + view.setStartTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") * 10); + view.setEndTimeMs((long) getValue(valueTelemetryOfDevices, "lastActivityTime") / 10); + EntityView savedView = doPost("/api/entityView", view, EntityView.class); + + List> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + assertEquals(0, values.size()); + } + + private Set getAttributesByKeys(String stringKV) throws Exception { + String viewDeviceId = testDevice.getId().getId().toString(); + DeviceCredentials deviceCredentials = + doGet("/api/device/" + viewDeviceId + "/credentials", DeviceCredentials.class); + assertEquals(testDevice.getId(), deviceCredentials.getDeviceId()); + + String accessToken = deviceCredentials.getCredentialsId(); + assertNotNull(accessToken); + + String clientId = MqttAsyncClient.generateClientId(); + MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId); + + MqttConnectOptions options = new MqttConnectOptions(); + options.setUserName(accessToken); + client.connect(options); + Thread.sleep(3000); + + MqttMessage message = new MqttMessage(); + message.setPayload((stringKV).getBytes()); + client.publish("v1/devices/me/attributes", message); + Thread.sleep(1000); + + return new HashSet<>(doGetAsync("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", List.class)); + } + + private Object getValue(List> values, String stringValue) { + return values.size() == 0 ? null : + values.stream() + .filter(value -> value.get("key").equals(stringValue)) + .findFirst().get().get("value"); + } + + private EntityView getNewSavedEntityView(String name) throws Exception { + EntityView view = new EntityView(); + view.setEntityId(testDevice.getId()); + view.setTenantId(savedTenant.getId()); + view.setName(name); + view.setKeys(telemetry); + return doPost("/api/entityView", view, EntityView.class); + } + + private Customer getNewCustomer(String title) { + Customer customer = new Customer(); + customer.setTitle(title); + return customer; + } + + private Tenant getNewTenant(String title) { + Tenant tenant = new Tenant(); + tenant.setTitle(title); + return tenant; + } + + private List fillListOf(int limit, String partOfName, String urlTemplate) throws Exception { + List views = new ArrayList<>(); + for (EntityView view : fillListOf(limit, partOfName)) { + views.add(doPost(urlTemplate + view.getId().getId().toString(), EntityView.class)); + } + return views; + } + + private List fillListOf(int limit, String partOfName) throws Exception { + List viewNames = new ArrayList<>(); + for (int i = 0; i < limit; i++) { + String fullName = partOfName + ' ' + RandomStringUtils.randomAlphanumeric(15); + fullName = i % 2 == 0 ? fullName.toLowerCase() : fullName.toUpperCase(); + EntityView view = getNewSavedEntityView(fullName); + Customer customer = getNewCustomer("Test customer " + String.valueOf(Math.random())); + view.setCustomerId(doPost("/api/customer", customer, Customer.class).getId()); + viewNames.add(doPost("/api/entityView", view, EntityView.class)); + } + return viewNames; + } + + private List loadListOf(TextPageLink pageLink, String urlTemplate) throws Exception { + List loadedItems = new ArrayList<>(); + TextPageData pageData; + do { + pageData = doGetTypedWithPageLink(urlTemplate, new TypeReference>() { + }, pageLink); + loadedItems.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + return loadedItems; + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java index 2e8848305f..f378437312 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerNoSqlTestSuite.java @@ -32,7 +32,8 @@ public class ControllerNoSqlTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), new ClassPathCQLDataSet("cassandra/system-data.cql", false, false), new ClassPathCQLDataSet("cassandra/system-test.cql", false, false)), "cassandra-test.yaml", 30000l); diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index f316051212..cdfa00136c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -24,13 +24,13 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ - "org.thingsboard.server.controller.sql.*SqlTest", + "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql"), "sql/drop-all-tables.sql", "sql-test.properties"); } diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java new file mode 100644 index 0000000000..404e4e253f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/nosql/EntityViewControllerNoSqlTest.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2018 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.nosql; + +import org.thingsboard.server.controller.BaseEntityViewControllerTest; +import org.thingsboard.server.dao.service.DaoNoSqlTest; + +/** + * Created by Victor Basanets on 8/27/2017. + */ +@DaoNoSqlTest +public class EntityViewControllerNoSqlTest extends BaseEntityViewControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java new file mode 100644 index 0000000000..76d99250c1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/EntityViewControllerSqlTest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2018 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.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.controller.BaseEntityViewControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Arrays; + +/** + * Created by Victor Basanets on 8/27/2017. + */ +@DaoSqlTest +public class EntityViewControllerSqlTest extends BaseEntityViewControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java index c4a969b1a9..2bb8a81ef2 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java @@ -32,7 +32,8 @@ public class MqttNoSqlTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)), "cassandra-test.yaml", 30000l); } diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java index 5ddbb67c97..1389c7edde 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java @@ -15,11 +15,9 @@ */ package org.thingsboard.server.mqtt; -import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.runner.RunWith; -import org.thingsboard.server.dao.CustomCassandraCQLUnit; import org.thingsboard.server.dao.CustomSqlUnit; import java.util.Arrays; @@ -31,7 +29,7 @@ public class MqttSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql"), "sql/drop-all-tables.sql", "sql-test.properties"); } diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java index bffe4913ef..c1d1e6cbaf 100644 --- a/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineNoSqlTestSuite.java @@ -35,7 +35,8 @@ public class RuleEngineNoSqlTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)), "cassandra-test.yaml", 30000l); diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java index 7b13e2fd54..e09d820777 100644 --- a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java @@ -30,7 +30,7 @@ public class RuleEngineSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql"), "sql/drop-all-tables.sql", "sql-test.properties"); } diff --git a/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java index 70e3fe2147..ebde304fd6 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemNoSqlTestSuite.java @@ -34,7 +34,8 @@ public class SystemNoSqlTestSuite { public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), new ClassPathCQLDataSet("cassandra/system-data.cql", false, false)), "cassandra-test.yaml", 30000l); } diff --git a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java index 97c67491f1..8ca6dcc877 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemSqlTestSuite.java @@ -31,7 +31,7 @@ public class SystemSqlTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql"), "sql/drop-all-tables.sql", "sql-test.properties"); 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 21de402721..698a69ef6b 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 @@ -20,4 +20,5 @@ public class CacheConstants { public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; public static final String ASSET_CACHE = "assets"; + public static final String ENTITY_VIEW_CACHE = "entityViews"; } 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 fe9c018718..ef4994adda 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; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java new file mode 100644 index 0000000000..25b066d2ef --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; + +/** + * Created by Victor Basanets on 8/27/2017. + */ + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class EntityView extends SearchTextBasedWithAdditionalInfo + implements HasName, HasTenantId, HasCustomerId { + + private static final long serialVersionUID = 5582010124562018986L; + + private EntityId entityId; + private TenantId tenantId; + private CustomerId customerId; + private String name; + private TelemetryEntityView keys; + private long startTimeMs; + private long endTimeMs; + + public EntityView() { + super(); + } + + public EntityView(EntityViewId id) { + super(id); + } + + public EntityView(EntityView entityView) { + super(entityView); + } + + @Override + public String getSearchText() { + return getName() /*What the ...*/; + } + + @Override + public CustomerId getCustomerId() { + return customerId; + } + + @Override + public String getName() { + return name; + } + + @Override + public TenantId getTenantId() { + return tenantId; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java new file mode 100644 index 0000000000..752eafc6ad --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; + +@Data +public class EntityViewSearchQuery { + + private RelationsSearchParameters parameters; + private String relationType; + + public EntityRelationsQuery toEntitySearchQuery() { + EntityRelationsQuery query = new EntityRelationsQuery(); + query.setParameters(parameters); + query.setFilters( + Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(EntityType.ENTITY_VIEW)))); + return query; + } +} 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 ed4cf2f784..4e35c0b2df 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 @@ -57,6 +57,8 @@ public class EntityIdFactory { return new RuleChainId(uuid); case RULE_NODE: return new RuleNodeId(uuid); + case ENTITY_VIEW: + return new EntityViewId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java new file mode 100644 index 0000000000..459dd990a4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +/** + * Created by Victor Basanets on 8/27/2017. + */ +public class EntityViewId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public EntityViewId(@JsonProperty("id") UUID id) { + super(id); + } + + public static EntityViewId fromString(String entityViewID) { + return new EntityViewId(UUID.fromString(entityViewID)); + } + + @Override + public EntityType getEntityType() { + return EntityType.ENTITY_VIEW; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java index 3c48adfc18..3e4e8ef7a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java @@ -42,4 +42,8 @@ public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery { this(key, startTs, endTs, endTs - startTs, 1, Aggregation.AVG, "DESC"); } + public BaseReadTsKvQuery(String key, long startTs, long endTs, int limit, String orderBy) { + this(key, startTs, endTs, endTs - startTs, limit, Aggregation.NONE, orderBy); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java new file mode 100644 index 0000000000..1c32579b72 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2018 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.objects; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Victor Basanets on 9/05/2017. + */ +@Data +@NoArgsConstructor +public class AttributesEntityView { + + private List cs = new ArrayList<>(); + private List ss = new ArrayList<>(); + private List sh = new ArrayList<>(); + + public AttributesEntityView(List cs, + List ss, + List sh) { + + this.cs = new ArrayList<>(cs); + this.ss = new ArrayList<>(ss); + this.sh = new ArrayList<>(sh); + } + + public AttributesEntityView(AttributesEntityView obj) { + this(obj.getCs(), obj.getSs(), obj.getSh()); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java new file mode 100644 index 0000000000..c899c65590 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2018 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.objects; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Victor Basanets on 9/05/2017. + */ +@Data +@NoArgsConstructor +public class TelemetryEntityView { + + private List timeseries; + private AttributesEntityView attributes; + + public TelemetryEntityView(List timeseries, AttributesEntityView attributes) { + + this.timeseries = new ArrayList<>(timeseries); + this.attributes = attributes; + } + + public TelemetryEntityView(TelemetryEntityView obj) { + this(obj.getTimeseries(), obj.getAttributes()); + } +} 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 6c8b60914e..f35b89069c 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,6 +30,7 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetSearchQuery; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -76,6 +78,9 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ @Autowired private CustomerDao customerDao; + @Autowired + private EntityViewService entityViewService; + @Autowired private CacheManager cacheManager; @@ -130,11 +135,21 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ validateId(assetId, INCORRECT_ASSET_ID + assetId); deleteEntityRelations(assetId); - Cache cache = cacheManager.getCache(ASSET_CACHE); Asset asset = assetDao.findById(assetId.getId()); + try { + List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(asset.getTenantId(), assetId).get(); + if (entityViews != null && !entityViews.isEmpty()) { + throw new DataValidationException("Can't delete asset that is assigned to entity views!"); + } + } catch (Exception e) { + log.error("Exception while finding entity views for assetId [{}]", assetId, e); + throw new RuntimeException("Exception while finding entity views for assetId [" + assetId + "]", e); + } + List list = new ArrayList<>(); list.add(asset.getTenantId()); list.add(asset.getName()); + Cache cache = cacheManager.getCache(ASSET_CACHE); cache.evict(list); assetDao.removeById(assetId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java index 567523228a..19409ad146 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraCluster.java @@ -17,12 +17,12 @@ package org.thingsboard.server.dao.cassandra; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlDao; +import org.thingsboard.server.dao.util.NoSqlAnyDao; import javax.annotation.PostConstruct; @Component -@NoSqlDao +@NoSqlAnyDao public class CassandraCluster extends AbstractCassandraCluster { @Value("${cassandra.keyspace_name}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java index 02968078f1..247a204ee5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraInstallCluster.java @@ -17,12 +17,12 @@ package org.thingsboard.server.dao.cassandra; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlDao; +import org.thingsboard.server.dao.util.NoSqlAnyDao; import javax.annotation.PostConstruct; @Component -@NoSqlDao +@NoSqlAnyDao @Profile("install") public class CassandraInstallCluster extends AbstractCassandraCluster { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java index 474cad7c6a..1f09342ca1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraQueryOptions.java @@ -21,14 +21,14 @@ import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlDao; +import org.thingsboard.server.dao.util.NoSqlAnyDao; import javax.annotation.PostConstruct; @Component @Configuration @Data -@NoSqlDao +@NoSqlAnyDao public class CassandraQueryOptions { @Value("${cassandra.query.default_fetch_size}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java index 8171ccc07f..15263c8b34 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/CassandraSocketOptions.java @@ -20,14 +20,14 @@ import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; -import org.thingsboard.server.dao.util.NoSqlDao; +import org.thingsboard.server.dao.util.NoSqlAnyDao; import javax.annotation.PostConstruct; @Component @Configuration @Data -@NoSqlDao +@NoSqlAnyDao public class CassandraSocketOptions { @Value("${cassandra.socket.connect_timeout}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 36b250d218..a9b8bfe181 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -32,6 +32,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; @@ -69,6 +70,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom @Autowired private DeviceService deviceService; + @Autowired + private EntityViewService entityViewService; + @Autowired private DashboardService dashboardService; @@ -111,6 +115,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom throw new IncorrectParameterException("Unable to delete non-existent customer."); } dashboardService.unassignCustomerDashboards(customerId); + entityViewService.unassignCustomerEntityViews(customer.getTenantId(), customerId); assetService.unassignCustomerAssets(customer.getTenantId(), customerId); deviceService.unassignCustomerDevices(customer.getTenantId(), customerId); userService.deleteCustomerUsers(customer.getTenantId(), customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 3930e3a94f..6f9ea62244 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; @@ -45,6 +46,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -86,6 +88,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceCredentialsService deviceCredentialsService; + @Autowired + private EntityViewService entityViewService; + @Autowired private CacheManager cacheManager; @@ -145,18 +150,31 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override public void deleteDevice(DeviceId deviceId) { log.trace("Executing deleteDevice [{}]", deviceId); - Cache cache = cacheManager.getCache(DEVICE_CACHE); validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + + Device device = deviceDao.findById(deviceId.getId()); + try { + List entityViews = entityViewService.findEntityViewsByTenantIdAndEntityIdAsync(device.getTenantId(), deviceId).get(); + if (entityViews != null && !entityViews.isEmpty()) { + throw new DataValidationException("Can't delete device that is assigned to entity views!"); + } + } catch (Exception e) { + log.error("Exception while finding entity views for deviceId [{}]", deviceId, e); + throw new RuntimeException("Exception while finding entity views for deviceId [" + deviceId + "]", e); + } + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId); if (deviceCredentials != null) { deviceCredentialsService.deleteDeviceCredentials(deviceCredentials); } deleteEntityRelations(deviceId); - Device device = deviceDao.findById(deviceId.getId()); + List list = new ArrayList<>(); list.add(device.getTenantId()); list.add(device.getName()); + Cache cache = cacheManager.getCache(DEVICE_CACHE); cache.evict(list); + deviceDao.removeById(deviceId.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java new file mode 100644 index 0000000000..a03dd89815 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/CassandraEntityViewDao.java @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import com.datastax.driver.core.Statement; +import com.datastax.driver.core.querybuilder.Select; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.EntitySubtypeEntity; +import org.thingsboard.server.dao.model.nosql.EntityViewEntity; +import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; +import org.thingsboard.server.dao.util.NoSqlDao; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_PROPERTY; + +/** + * Created by Victor Basanets on 9/06/2017. + */ +@Component +@Slf4j +@NoSqlDao +public class CassandraEntityViewDao extends CassandraAbstractSearchTextDao implements EntityViewDao { + + @Override + protected Class getColumnFamilyClass() { + return EntityViewEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return ENTITY_VIEW_TABLE_FAMILY_NAME; + } + + @Override + public EntityView save(EntityView domain) { + EntityView savedEntityView = super.save(domain); + EntitySubtype entitySubtype = new EntitySubtype(savedEntityView.getTenantId(), EntityType.ENTITY_VIEW, + savedEntityView.getId().getEntityType().toString()); + EntitySubtypeEntity entitySubtypeEntity = new EntitySubtypeEntity(entitySubtype); + Statement saveStatement = cluster.getMapper(EntitySubtypeEntity.class).saveQuery(entitySubtypeEntity); + executeWrite(saveStatement); + return savedEntityView; + } + + @Override + public List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}] and pageLink [{}]", tenantId, pageLink); + List entityViewEntities = + findPageWithTextSearch(ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, + Collections.singletonList(eq(TENANT_ID_PROPERTY, tenantId)), pageLink); + log.trace("Found entity views [{}] by tenantId [{}] and pageLink [{}]", + entityViewEntities, tenantId, pageLink); + return DaoUtil.convertDataList(entityViewEntities); + } + + @Override + public Optional findEntityViewByTenantIdAndName(UUID tenantId, String name) { + Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_NAME).where(); + query.and(eq(ENTITY_VIEW_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(ENTITY_VIEW_NAME_PROPERTY, name)); + return Optional.ofNullable(DaoUtil.getData(findOneByStatement(query))); + } + + @Override + public List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { + log.debug("Try to find entity views by tenantId [{}], customerId[{}] and pageLink [{}]", + tenantId, customerId, pageLink); + List entityViewEntities = findPageWithTextSearch( + ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF, + Arrays.asList(eq(CUSTOMER_ID_PROPERTY, customerId), eq(TENANT_ID_PROPERTY, tenantId)), + pageLink); + log.trace("Found find entity views [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", + entityViewEntities, tenantId, customerId, pageLink); + return DaoUtil.convertDataList(entityViewEntities); + } + + @Override + public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { + log.debug("Try to find entity views by tenantId [{}] and entityId [{}]", tenantId, entityId); + Select.Where query = select().from(ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF).where(); + query.and(eq(TENANT_ID_PROPERTY, tenantId)); + query.and(eq(ENTITY_ID_COLUMN, entityId)); + return findListByStatementAsync(query); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java new file mode 100644 index 0000000000..ba433855e2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.Dao; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Created by Victor Basanets on 8/28/2017. + */ +public interface EntityViewDao extends Dao { + + /** + * Save or update device object + * + * @param entityView the entity-view object + * @return saved entity-view object + */ + EntityView save(EntityView entityView); + + /** + * Find entity views by tenantId and page link. + * + * @param tenantId the tenantId + * @param pageLink the page link + * @return the list of entity view objects + */ + List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink); + + /** + * Find entity views by tenantId and entity view name. + * + * @param tenantId the tenantId + * @param name the entity view name + * @return the optional entity view object + */ + Optional findEntityViewByTenantIdAndName(UUID tenantId, String name); + + /** + * Find entity views by tenantId, customerId and page link. + * + * @param tenantId the tenantId + * @param customerId the customerId + * @param pageLink the page link + * @return the list of entity view objects + */ + List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, + UUID customerId, + TextPageLink pageLink); + + + ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java new file mode 100644 index 0000000000..19f326c0bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; + +import java.util.List; + +/** + * Created by Victor Basanets on 8/27/2017. + */ +public interface EntityViewService { + + EntityView saveEntityView(EntityView entityView); + + EntityView assignEntityViewToCustomer(EntityViewId entityViewId, CustomerId customerId); + + EntityView unassignEntityViewFromCustomer(EntityViewId entityViewId); + + void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId); + + EntityView findEntityViewById(EntityViewId entityViewId); + + TextPageData findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink); + + TextPageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + + ListenableFuture> findEntityViewsByQuery(EntityViewSearchQuery query); + + ListenableFuture findEntityViewByIdAsync(EntityViewId entityViewId); + + ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId); + + void deleteEntityView(EntityViewId entityViewId); + + void deleteEntityViewsByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java new file mode 100644 index 0000000000..2c5ec75485 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -0,0 +1,356 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.page.TextPageData; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerDao; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.tenant.TenantDao; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.CacheConstants.ENTITY_VIEW_CACHE; +import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + +/** + * Created by Victor Basanets on 8/28/2017. + */ +@Service +@Slf4j +public class EntityViewServiceImpl extends AbstractEntityService implements EntityViewService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; + public static final String INCORRECT_ENTITY_VIEW_ID = "Incorrect entityViewId "; + + @Autowired + private EntityViewDao entityViewDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private CustomerDao customerDao; + + @Autowired + private AttributesService attributesService; + + @Autowired + private CacheManager cacheManager; + + @Caching(evict = { + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.tenantId, #entityView.entityId}"), + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityView.id}")}) + @Override + public EntityView saveEntityView(EntityView entityView) { + log.trace("Executing save entity view [{}]", entityView); + entityViewValidator.validate(entityView); + EntityView savedEntityView = entityViewDao.save(entityView); + + List>> futures = new ArrayList<>(); + if (savedEntityView.getKeys() != null) { + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs())); + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs())); + futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh())); + } + for (ListenableFuture> future : futures) { + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to copy attributes to entity view", e); + throw new RuntimeException("Failed to copy attributes to entity view", e); + } + } + return savedEntityView; + } + + @Override + public EntityView assignEntityViewToCustomer(EntityViewId entityViewId, CustomerId customerId) { + EntityView entityView = findEntityViewById(entityViewId); + entityView.setCustomerId(customerId); + return saveEntityView(entityView); + } + + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") + @Override + public EntityView unassignEntityViewFromCustomer(EntityViewId entityViewId) { + EntityView entityView = findEntityViewById(entityViewId); + entityView.setCustomerId(null); + return saveEntityView(entityView); + } + + @Override + public void unassignCustomerEntityViews(TenantId tenantId, CustomerId customerId) { + log.trace("Executing unassignCustomerEntityViews, tenantId [{}], customerId [{}]", tenantId, customerId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + new CustomerEntityViewsUnAssigner(tenantId).removeEntities(customerId); + } + + @Cacheable(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") + @Override + public EntityView findEntityViewById(EntityViewId entityViewId) { + log.trace("Executing findEntityViewById [{}]", entityViewId); + validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + return entityViewDao.findById(entityViewId.getId()); + } + + @Override + public TextPageData findEntityViewByTenantId(TenantId tenantId, TextPageLink pageLink) { + log.trace("Executing findEntityViewsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); + List entityViews = entityViewDao.findEntityViewsByTenantId(tenantId.getId(), pageLink); + return new TextPageData<>(entityViews, pageLink); + } + + @Override + public TextPageData findEntityViewsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, + TextPageLink pageLink) { + log.trace("Executing findEntityViewByTenantIdAndCustomerId, tenantId [{}], customerId [{}]," + + " pageLink [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); + validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); + List entityViews = entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), + customerId.getId(), pageLink); + return new TextPageData<>(entityViews, pageLink); + } + + @Override + public ListenableFuture> findEntityViewsByQuery(EntityViewSearchQuery query) { + ListenableFuture> relations = relationService.findByQuery(query.toEntitySearchQuery()); + ListenableFuture> entityViews = Futures.transformAsync(relations, r -> { + EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); + List> futures = new ArrayList<>(); + for (EntityRelation relation : r) { + EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); + if (entityId.getEntityType() == EntityType.ENTITY_VIEW) { + futures.add(findEntityViewByIdAsync(new EntityViewId(entityId.getId()))); + } + } + return Futures.successfulAsList(futures); + }); + return entityViews; + } + + @Override + public ListenableFuture findEntityViewByIdAsync(EntityViewId entityViewId) { + log.trace("Executing findEntityViewById [{}]", entityViewId); + validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + return entityViewDao.findByIdAsync(entityViewId.getId()); + } + + @Override + public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) { + log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(entityId.getId(), "Incorrect entityId" + entityId); + + List tenantIdAndEntityId = new ArrayList<>(); + tenantIdAndEntityId.add(tenantId); + tenantIdAndEntityId.add(entityId); + + Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE); + List fromCache = cache.get(tenantIdAndEntityId, List.class); + if (fromCache != null) { + return Futures.immediateFuture(fromCache); + } else { + ListenableFuture> entityViewsFuture = entityViewDao.findEntityViewsByTenantIdAndEntityIdAsync(tenantId.getId(), entityId.getId()); + Futures.addCallback(entityViewsFuture, + new FutureCallback>() { + @Override + public void onSuccess(@Nullable List result) { + cache.putIfAbsent(tenantIdAndEntityId, result); + } + @Override + public void onFailure(Throwable t) { + log.error("Error while finding entity views by tenantId and entityId", t); + } + }); + return entityViewsFuture; + } + } + + @CacheEvict(cacheNames = ENTITY_VIEW_CACHE, key = "{#entityViewId}") + @Override + public void deleteEntityView(EntityViewId entityViewId) { + log.trace("Executing deleteEntityView [{}]", entityViewId); + validateId(entityViewId, INCORRECT_ENTITY_VIEW_ID + entityViewId); + deleteEntityRelations(entityViewId); + EntityView entityView = entityViewDao.findById(entityViewId.getId()); + cacheManager.getCache(ENTITY_VIEW_CACHE).evict(Arrays.asList(entityView.getTenantId(), entityView.getEntityId())); + entityViewDao.removeById(entityViewId.getId()); + } + + @Override + public void deleteEntityViewsByTenantId(TenantId tenantId) { + log.trace("Executing deleteEntityViewsByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + tenantEntityViewRemover.removeEntities(tenantId); + } + + private ListenableFuture> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection keys) { + if (keys != null && !keys.isEmpty()) { + ListenableFuture> getAttrFuture = attributesService.find(entityView.getEntityId(), scope, keys); + return Futures.transform(getAttrFuture, attributeKvEntries -> { + List filteredAttributes = new ArrayList<>(); + if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) { + filteredAttributes = + attributeKvEntries.stream() + .filter(attributeKvEntry -> { + long startTime = entityView.getStartTimeMs(); + long endTime = entityView.getEndTimeMs(); + long lastUpdateTs = attributeKvEntry.getLastUpdateTs(); + return startTime == 0 && endTime == 0 || + (endTime == 0 && startTime < lastUpdateTs) || + (startTime == 0 && endTime > lastUpdateTs) + ? true : startTime < lastUpdateTs && endTime > lastUpdateTs; + }).collect(Collectors.toList()); + } + try { + return attributesService.save(entityView.getId(), scope, filteredAttributes).get(); + } catch (InterruptedException | ExecutionException e) { + log.error("Failed to copy attributes to entity view", e); + throw new RuntimeException("Failed to copy attributes to entity view", e); + } + }); + } else { + return Futures.immediateFuture(null); + } + } + + private DataValidator entityViewValidator = + new DataValidator() { + + @Override + protected void validateCreate(EntityView entityView) { + entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName()) + .ifPresent(e -> { + throw new DataValidationException("Entity view with such name already exists!"); + }); + } + + @Override + protected void validateUpdate(EntityView entityView) { + entityViewDao.findEntityViewByTenantIdAndName(entityView.getTenantId().getId(), entityView.getName()) + .ifPresent(e -> { + if (!e.getUuidId().equals(entityView.getUuidId())) { + throw new DataValidationException("Entity view with such name already exists!"); + } + }); + } + + @Override + protected void validateDataImpl(EntityView entityView) { + if (StringUtils.isEmpty(entityView.getName())) { + throw new DataValidationException("Entity view name should be specified!"); + } + if (entityView.getTenantId() == null) { + throw new DataValidationException("Entity view should be assigned to tenant!"); + } else { + Tenant tenant = tenantDao.findById(entityView.getTenantId().getId()); + if (tenant == null) { + throw new DataValidationException("Entity view is referencing to non-existent tenant!"); + } + } + if (entityView.getCustomerId() == null) { + entityView.setCustomerId(new CustomerId(NULL_UUID)); + } else if (!entityView.getCustomerId().getId().equals(NULL_UUID)) { + Customer customer = customerDao.findById(entityView.getCustomerId().getId()); + if (customer == null) { + throw new DataValidationException("Can't assign entity view to non-existent customer!"); + } + if (!customer.getTenantId().getId().equals(entityView.getTenantId().getId())) { + throw new DataValidationException("Can't assign entity view to customer from different tenant!"); + } + } + } + }; + + private PaginatedRemover tenantEntityViewRemover = + new PaginatedRemover() { + + @Override + protected List findEntities(TenantId id, TextPageLink pageLink) { + return entityViewDao.findEntityViewsByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(EntityView entity) { + deleteEntityView(new EntityViewId(entity.getUuidId())); + } + }; + + private class CustomerEntityViewsUnAssigner extends PaginatedRemover { + + private TenantId tenantId; + + CustomerEntityViewsUnAssigner(TenantId tenantId) { + this.tenantId = tenantId; + } + + @Override + protected List findEntities(CustomerId id, TextPageLink pageLink) { + return entityViewDao.findEntityViewsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); + } + + @Override + protected void removeEntity(EntityView entity) { + unassignEntityViewFromCustomer(new EntityViewId(entity.getUuidId())); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3a934eb5d2..a487ede93a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -52,7 +52,6 @@ public class ModelConstants { public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts"; - /** * Cassandra user constants. */ @@ -130,12 +129,12 @@ public class ModelConstants { * Cassandra device constants. */ public static final String DEVICE_COLUMN_FAMILY_NAME = "device"; + public static final String DEVICE_FAMILY_NAME = "device"; public static final String DEVICE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String DEVICE_NAME_PROPERTY = "name"; public static final String DEVICE_TYPE_PROPERTY = "type"; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; - public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; @@ -143,6 +142,23 @@ public class ModelConstants { public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; + /** + * Cassandra entityView constants. + */ + public static final String ENTITY_VIEW_TABLE_FAMILY_NAME = "entity_views"; + public static final String ENTITY_VIEW_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN; + public static final String ENTITY_VIEW_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String ENTITY_VIEW_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String ENTITY_VIEW_NAME_PROPERTY = DEVICE_NAME_PROPERTY; + public static final String ENTITY_VIEW_BY_TENANT_AND_CUSTOMER_CF = "entity_view_by_tenant_and_customer"; + public static final String ENTITY_VIEW_BY_TENANT_AND_ENTITY_ID_CF = "entity_view_by_tenant_and_entity_id"; + public static final String ENTITY_VIEW_KEYS_PROPERTY = "keys"; + public static final String ENTITY_VIEW_START_TS_PROPERTY = "start_ts"; + public static final String ENTITY_VIEW_END_TS_PROPERTY = "end_ts"; + public static final String ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String ENTITY_VIEW_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "entity_view_by_tenant_and_search_text"; + public static final String ENTITY_VIEW_BY_TENANT_AND_NAME = "entity_view_by_tenant_and_name"; + /** * Cassandra audit log constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java new file mode 100644 index 0000000000..eb7f4fe65e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java @@ -0,0 +1,157 @@ +/** + * Copyright © 2016-2018 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.nosql; + +import com.datastax.driver.core.utils.UUIDs; +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; + +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import java.io.IOException; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY; + +/** + * Created by Victor Basanets on 8/31/2017. + */ +@Data +@Table(name = ENTITY_VIEW_TABLE_FAMILY_NAME) +@EqualsAndHashCode +@ToString +@Slf4j +public class EntityViewEntity implements SearchTextEntity { + + @PartitionKey(value = 0) + @Column(name = ID_PROPERTY) + private UUID id; + + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_TYPE_PROPERTY) + private EntityType entityType; + + @PartitionKey(value = 1) + @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY) + private UUID tenantId; + + @PartitionKey(value = 2) + @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) + private UUID customerId; + + @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY) + private String keys; + + @Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY) + private long startTs; + + @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) + private long endTs; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + private static final ObjectMapper mapper = new ObjectMapper(); + + public EntityViewEntity() { + super(); + } + + public EntityViewEntity(EntityView entityView) { + if (entityView.getId() != null) { + this.id = entityView.getId().getId(); + } + if (entityView.getEntityId() != null) { + this.entityId = entityView.getEntityId().getId(); + this.entityType = entityView.getEntityId().getEntityType(); + } + if (entityView.getTenantId() != null) { + this.tenantId = entityView.getTenantId().getId(); + } + if (entityView.getCustomerId() != null) { + this.customerId = entityView.getCustomerId().getId(); + } + this.name = entityView.getName(); + try { + this.keys = mapper.writeValueAsString(entityView.getKeys()); + } catch (IOException e) { + log.error("Unable to serialize entity view keys!", e); + } + this.startTs = entityView.getStartTimeMs(); + this.endTs = entityView.getEndTimeMs(); + this.searchText = entityView.getSearchText(); + this.additionalInfo = entityView.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public EntityView toData() { + EntityView entityView = new EntityView(new EntityViewId(id)); + entityView.setCreatedTime(UUIDs.unixTimestamp(id)); + if (entityId != null) { + entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), entityId.toString())); + } + if (tenantId != null) { + entityView.setTenantId(new TenantId(tenantId)); + } + if (customerId != null) { + entityView.setCustomerId(new CustomerId(customerId)); + } + entityView.setName(name); + try { + entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); + } catch (IOException e) { + log.error("Unable to read entity view keys!", e); + } + entityView.setStartTimeMs(startTs); + entityView.setEndTimeMs(endTs); + entityView.setAdditionalInfo(additionalInfo); + return entityView; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java new file mode 100644 index 0000000000..60f6951b80 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityViewEntity.java @@ -0,0 +1,158 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.model.sql; + +import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.objects.TelemetryEntityView; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.io.IOException; + +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_PROPERTY; + +/** + * Created by Victor Basanets on 8/30/2017. + */ + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.ENTITY_VIEW_TABLE_FAMILY_NAME) +@Slf4j +public class EntityViewEntity extends BaseSqlEntity implements SearchTextEntity { + + @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY) + private String entityId; + + @Enumerated(EnumType.STRING) + @Column(name = ENTITY_TYPE_PROPERTY) + private EntityType entityType; + + @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY) + private String tenantId; + + @Column(name = ModelConstants.ENTITY_VIEW_CUSTOMER_ID_PROPERTY) + private String customerId; + + @Column(name = ModelConstants.ENTITY_VIEW_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.ENTITY_VIEW_KEYS_PROPERTY) + private String keys; + + @Column(name = ModelConstants.ENTITY_VIEW_START_TS_PROPERTY) + private long startTs; + + @Column(name = ModelConstants.ENTITY_VIEW_END_TS_PROPERTY) + private long endTs; + + @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) + private String searchText; + + @Type(type = "json") + @Column(name = ModelConstants.ENTITY_VIEW_ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + private static final ObjectMapper mapper = new ObjectMapper(); + + public EntityViewEntity() { + super(); + } + + public EntityViewEntity(EntityView entityView) { + if (entityView.getId() != null) { + this.setId(entityView.getId().getId()); + } + if (entityView.getEntityId() != null) { + this.entityId = toString(entityView.getEntityId().getId()); + this.entityType = entityView.getEntityId().getEntityType(); + } + if (entityView.getTenantId() != null) { + this.tenantId = toString(entityView.getTenantId().getId()); + } + if (entityView.getCustomerId() != null) { + this.customerId = toString(entityView.getCustomerId().getId()); + } + this.name = entityView.getName(); + try { + this.keys = mapper.writeValueAsString(entityView.getKeys()); + } catch (IOException e) { + log.error("Unable to serialize entity view keys!", e); + } + this.startTs = entityView.getStartTimeMs(); + this.endTs = entityView.getEndTimeMs(); + this.searchText = entityView.getSearchText(); + this.additionalInfo = entityView.getAdditionalInfo(); + } + + @Override + public String getSearchTextSource() { + return name; + } + + @Override + public void setSearchText(String searchText) { + this.searchText = searchText; + } + + @Override + public EntityView toData() { + EntityView entityView = new EntityView(new EntityViewId(getId())); + entityView.setCreatedTime(UUIDs.unixTimestamp(getId())); + + if (entityId != null) { + entityView.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString())); + } + if (tenantId != null) { + entityView.setTenantId(new TenantId(toUUID(tenantId))); + } + if (customerId != null) { + entityView.setCustomerId(new CustomerId(toUUID(customerId))); + } + entityView.setName(name); + try { + entityView.setKeys(mapper.readValue(keys, TelemetryEntityView.class)); + } catch (IOException e) { + log.error("Unable to read entity view keys!", e); + } + entityView.setStartTimeMs(startTs); + entityView.setEndTimeMs(endTs); + entityView.setAdditionalInfo(additionalInfo); + return entityView; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java new file mode 100644 index 0000000000..0f5fdf5fba --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.model.sql.EntityViewEntity; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.List; + +/** + * Created by Victor Basanets on 8/31/2017. + */ +@SqlDao +public interface EntityViewRepository extends CrudRepository { + + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " + + "AND e.id > :idOffset ORDER BY e.id") + List findByTenantId(@Param("tenantId") String tenantId, + @Param("textSearch") String textSearch, + @Param("idOffset") String idOffset, + Pageable pageable); + + @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + + "AND e.customerId = :customerId " + + "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:searchText, '%')) " + + "AND e.id > :idOffset ORDER BY e.id") + List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, + @Param("customerId") String customerId, + @Param("searchText") String searchText, + @Param("idOffset") String idOffset, + Pageable pageable); + + EntityViewEntity findByTenantIdAndName(String tenantId, String name); + + List findAllByTenantIdAndEntityId(String tenantId, String entityId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java new file mode 100644 index 0000000000..912c9d5eed --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2018 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.entityview; + +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.entityview.EntityViewDao; +import org.thingsboard.server.dao.model.sql.EntityViewEntity; +import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; +import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUIDs; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; + +/** + * Created by Victor Basanets on 8/31/2017. + */ +@Component +@SqlDao +public class JpaEntityViewDao extends JpaAbstractSearchTextDao + implements EntityViewDao { + + @Autowired + private EntityViewRepository entityViewRepository; + + @Override + protected Class getEntityClass() { + return EntityViewEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return entityViewRepository; + } + + @Override + public List findEntityViewsByTenantId(UUID tenantId, TextPageLink pageLink) { + return DaoUtil.convertDataList( + entityViewRepository.findByTenantId( + fromTimeUUID(tenantId), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + + @Override + public Optional findEntityViewByTenantIdAndName(UUID tenantId, String name) { + return Optional.ofNullable( + DaoUtil.getData(entityViewRepository.findByTenantIdAndName(fromTimeUUID(tenantId), name))); + } + + @Override + public List findEntityViewsByTenantIdAndCustomerId(UUID tenantId, + UUID customerId, + TextPageLink pageLink) { + return DaoUtil.convertDataList( + entityViewRepository.findByTenantIdAndCustomerId( + fromTimeUUID(tenantId), + fromTimeUUID(customerId), + Objects.toString(pageLink.getTextSearch(), ""), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()) + )); + } + + @Override + public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(UUID tenantId, UUID entityId) { + return service.submit(() -> DaoUtil.convertDataList( + entityViewRepository.findAllByTenantIdAndEntityId(UUIDConverter.fromTimeUUID(tenantId), UUIDConverter.fromTimeUUID(entityId)))); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java index 761138649d..5bd9175628 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java @@ -44,6 +44,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; import org.thingsboard.server.dao.timeseries.TimeseriesDao; import org.thingsboard.server.dao.timeseries.TsInsertExecutorType; import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.dao.util.SqlTsDao; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -60,7 +61,7 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; @Component @Slf4j -@SqlDao +@SqlTsDao public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao { @Value("${sql.ts_inserts_executor_type}") 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 d92c9410e5..189c7139ac 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 @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; @@ -63,6 +64,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private DeviceService deviceService; + @Autowired + private EntityViewService entityViewService; + @Autowired private WidgetsBundleService widgetsBundleService; @@ -101,6 +105,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe customerService.deleteCustomersByTenantId(tenantId); widgetsBundleService.deleteWidgetsBundlesByTenantId(tenantId); dashboardService.deleteDashboardsByTenantId(tenantId); + entityViewService.deleteEntityViewsByTenantId(tenantId); assetService.deleteAssetsByTenantId(tenantId); deviceService.deleteDevicesByTenantId(tenantId); userService.deleteTenantAdmins(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 98e859f39f..264cdc8f8d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -21,15 +21,22 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -46,10 +53,21 @@ public class BaseTimeseriesService implements TimeseriesService { @Autowired private TimeseriesDao timeseriesDao; + @Autowired + private EntityViewService entityViewService; + @Override public ListenableFuture> findAll(EntityId entityId, List queries) { validate(entityId); queries.forEach(BaseTimeseriesService::validate); + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { + EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId); + List filteredQueries = + queries.stream() + .filter(query -> entityView.getKeys().getTimeseries().isEmpty() || entityView.getKeys().getTimeseries().contains(query.getKey())) + .collect(Collectors.toList()); + return timeseriesDao.findAllAsync(entityView.getEntityId(), updateQueriesForEntityView(entityView, filteredQueries)); + } return timeseriesDao.findAllAsync(entityId, queries); } @@ -58,6 +76,19 @@ public class BaseTimeseriesService implements TimeseriesService { validate(entityId); List> futures = Lists.newArrayListWithExpectedSize(keys.size()); keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key)); + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { + EntityView entityView = entityViewService.findEntityViewById((EntityViewId) entityId); + List filteredKeys = new ArrayList<>(keys); + if (!entityView.getKeys().getTimeseries().isEmpty()) { + filteredKeys.retainAll(entityView.getKeys().getTimeseries()); + } + List queries = + filteredKeys.stream() + .map(key -> new BaseReadTsKvQuery(key, entityView.getStartTimeMs(), entityView.getEndTimeMs(), 1, "ASC")) + .collect(Collectors.toList()); + + return timeseriesDao.findAllAsync(entityView.getEntityId(), updateQueriesForEntityView(entityView, queries)); + } keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key))); return Futures.allAsList(futures); } @@ -92,11 +123,24 @@ public class BaseTimeseriesService implements TimeseriesService { } private void saveAndRegisterFutures(List> futures, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { + if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { + throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Only read only"); + } futures.add(timeseriesDao.savePartition(entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); futures.add(timeseriesDao.saveLatest(entityId, tsKvEntry)); futures.add(timeseriesDao.save(entityId, tsKvEntry, ttl)); } + private List updateQueriesForEntityView(EntityView entityView, List queries) { + return queries.stream().map(query -> { + long startTs = entityView.getStartTimeMs() == 0 ? query.getStartTs() : entityView.getStartTimeMs(); + long endTs = entityView.getEndTimeMs() == 0 ? query.getEndTs() : entityView.getEndTimeMs(); + + return startTs <= query.getStartTs() && endTs >= query.getEndTs() ? query : + new BaseReadTsKvQuery(query.getKey(), startTs, endTs, query.getInterval(), query.getLimit(), query.getAggregation()); + }).collect(Collectors.toList()); + } + @Override public ListenableFuture> remove(EntityId entityId, List deleteTsKvQueries) { validate(entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 03d569d78e..709bfd5432 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.nosql.CassandraAbstractAsyncDao; import org.thingsboard.server.dao.util.NoSqlDao; +import org.thingsboard.server.dao.util.NoSqlTsDao; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -70,7 +71,7 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; */ @Component @Slf4j -@NoSqlDao +@NoSqlTsDao public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implements TimeseriesDao { private static final int MIN_AGGREGATION_STEP_MS = 1000; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java index 817845b54a..eab05b2fdb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java @@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Component @Slf4j -@NoSqlDao +@NoSqlAnyDao public class BufferedRateLimiter implements AsyncRateLimiter { private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java new file mode 100644 index 0000000000..a8049eec5f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlAnyDao.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2018 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.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +@ConditionalOnExpression("'${database.ts.type}'=='cassandra' || '${database.entities.type}'=='cassandra'") +public @interface NoSqlAnyDao { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java index 96dbdab758..c3a719d893 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlDao.java @@ -17,6 +17,6 @@ package org.thingsboard.server.dao.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -@ConditionalOnProperty(prefix = "database", value = "type", havingValue = "cassandra") +@ConditionalOnProperty(prefix = "database.entities", value = "type", havingValue = "cassandra") public @interface NoSqlDao { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java new file mode 100644 index 0000000000..748365fe41 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/NoSqlTsDao.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2018 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.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@ConditionalOnProperty(prefix = "database.ts", value = "type", havingValue = "cassandra") +public @interface NoSqlTsDao { +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java b/dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java index 3986f022d9..ab39c847b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/SqlDao.java @@ -17,6 +17,6 @@ package org.thingsboard.server.dao.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -@ConditionalOnProperty(prefix = "database", value = "type", havingValue = "sql") +@ConditionalOnProperty(prefix = "database.entities", value = "type", havingValue = "sql") public @interface SqlDao { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java b/dao/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java new file mode 100644 index 0000000000..0470486408 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/util/SqlTsDao.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2018 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.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@ConditionalOnProperty(prefix = "database.ts", value = "type", havingValue = "sql") +public @interface SqlTsDao { +} diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema-entities.cql similarity index 88% rename from dao/src/main/resources/cassandra/schema.cql rename to dao/src/main/resources/cassandra/schema-entities.cql index f03122ab6b..7ccd41f03a 100644 --- a/dao/src/main/resources/cassandra/schema.cql +++ b/dao/src/main/resources/cassandra/schema-entities.cql @@ -165,35 +165,55 @@ CREATE TABLE IF NOT EXISTS thingsboard.device ( CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS SELECT * from thingsboard.device - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND type IS NOT NULL + AND name IS NOT NULL + AND id IS NOT NULL PRIMARY KEY ( tenant_id, name, id, customer_id, type) WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS SELECT * from thingsboard.device - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND type IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL PRIMARY KEY ( tenant_id, search_text, id, customer_id, type) WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS SELECT * from thingsboard.device - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND type IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL PRIMARY KEY ( tenant_id, type, search_text, id, customer_id) WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS SELECT * from thingsboard.device - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND type IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL PRIMARY KEY ( customer_id, tenant_id, search_text, id, type ) WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC ); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS SELECT * from thingsboard.device - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND type IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL PRIMARY KEY ( customer_id, tenant_id, type, search_text, id ) WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC ); @@ -378,41 +398,6 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_tenant_and_searc PRIMARY KEY ( tenant_id, search_text, id ) WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); -CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) - entity_id timeuuid, - key text, - partition bigint, - ts bigint, - bool_v boolean, - str_v text, - long_v bigint, - dbl_v double, - PRIMARY KEY (( entity_type, entity_id, key, partition ), ts) -); - -CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) - entity_id timeuuid, - key text, - partition bigint, - PRIMARY KEY (( entity_type, entity_id, key ), partition) -) WITH CLUSTERING ORDER BY ( partition ASC ) - AND compaction = { 'class' : 'LeveledCompactionStrategy' }; - -CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) - entity_id timeuuid, - key text, - ts bigint, - bool_v boolean, - str_v text, - long_v bigint, - dbl_v double, - PRIMARY KEY (( entity_type, entity_id ), key) -) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }; - - CREATE TABLE IF NOT EXISTS thingsboard.attributes_kv_cf ( entity_type text, // (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, @@ -638,3 +623,62 @@ CREATE TABLE IF NOT EXISTS thingsboard.rule_node ( additional_info text, PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS thingsboard.entity_views ( + id timeuuid, + entity_id timeuuid, + entity_type text, + tenant_id timeuuid, + customer_id timeuuid, + name text, + keys text, + start_ts bigint, + end_ts bigint, + search_text text, + additional_info text, + PRIMARY KEY (id, entity_id, tenant_id, customer_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_name AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND entity_id IS NOT NULL + AND customer_id IS NOT NULL + AND name IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, name, id, customer_id, entity_id) + WITH CLUSTERING ORDER BY (name ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_search_text AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND entity_id IS NOT NULL + AND customer_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, search_text, id, customer_id, entity_id) + WITH CLUSTERING ORDER BY (search_text ASC, id DESC, customer_id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_customer AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND entity_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, customer_id, search_text, id, entity_id) + WITH CLUSTERING ORDER BY (customer_id DESC, search_text ASC, id DESC); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_entity_id AS + SELECT * + from thingsboard.entity_views + WHERE tenant_id IS NOT NULL + AND customer_id IS NOT NULL + AND entity_id IS NOT NULL + AND search_text IS NOT NULL + AND id IS NOT NULL + PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id) + WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC); \ No newline at end of file diff --git a/dao/src/main/resources/cassandra/schema-ts.cql b/dao/src/main/resources/cassandra/schema-ts.cql new file mode 100644 index 0000000000..a5c5ec2ff1 --- /dev/null +++ b/dao/src/main/resources/cassandra/schema-ts.cql @@ -0,0 +1,55 @@ +-- +-- Copyright © 2016-2018 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 KEYSPACE IF NOT EXISTS thingsboard +WITH replication = { + 'class' : 'SimpleStrategy', + 'replication_factor' : 1 +}; + +CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( + entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_id timeuuid, + key text, + partition bigint, + ts bigint, + bool_v boolean, + str_v text, + long_v bigint, + dbl_v double, + PRIMARY KEY (( entity_type, entity_id, key, partition ), ts) +); + +CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf ( + entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_id timeuuid, + key text, + partition bigint, + PRIMARY KEY (( entity_type, entity_id, key ), partition) +) WITH CLUSTERING ORDER BY ( partition ASC ) + AND compaction = { 'class' : 'LeveledCompactionStrategy' }; + +CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( + entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_id timeuuid, + key text, + ts bigint, + bool_v boolean, + str_v text, + long_v bigint, + dbl_v double, + PRIMARY KEY (( entity_type, entity_id ), key) +) WITH compaction = { 'class' : 'LeveledCompactionStrategy' }; diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema-entities.sql similarity index 90% rename from dao/src/main/resources/sql/schema.sql rename to dao/src/main/resources/sql/schema-entities.sql index 91e77da503..abd20abd40 100644 --- a/dao/src/main/resources/sql/schema.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -179,30 +179,6 @@ CREATE TABLE IF NOT EXISTS tenant ( zip varchar(255) ); -CREATE TABLE IF NOT EXISTS ts_kv ( - entity_type varchar(255) NOT NULL, - entity_id varchar(31) NOT NULL, - key varchar(255) NOT NULL, - ts bigint NOT NULL, - bool_v boolean, - str_v varchar(10000000), - long_v bigint, - dbl_v double precision, - CONSTRAINT ts_kv_unq_key UNIQUE (entity_type, entity_id, key, ts) -); - -CREATE TABLE IF NOT EXISTS ts_kv_latest ( - entity_type varchar(255) NOT NULL, - entity_id varchar(31) NOT NULL, - key varchar(255) NOT NULL, - ts bigint NOT NULL, - bool_v boolean, - str_v varchar(10000000), - long_v bigint, - dbl_v double precision, - CONSTRAINT ts_kv_latest_unq_key UNIQUE (entity_type, entity_id, key) -); - CREATE TABLE IF NOT EXISTS user_credentials ( id varchar(31) NOT NULL CONSTRAINT user_credentials_pkey PRIMARY KEY, activate_token varchar(255) UNIQUE, @@ -251,3 +227,17 @@ CREATE TABLE IF NOT EXISTS rule_node ( debug_mode boolean, search_text varchar(255) ); + +CREATE TABLE IF NOT EXISTS entity_views ( + id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY, + entity_id varchar(31), + entity_type varchar(255), + tenant_id varchar(31), + customer_id varchar(31), + name varchar(255), + keys varchar(255), + start_ts bigint, + end_ts bigint, + search_text varchar(255), + additional_info varchar +); diff --git a/dao/src/main/resources/sql/schema-ts.sql b/dao/src/main/resources/sql/schema-ts.sql new file mode 100644 index 0000000000..53bc15acc3 --- /dev/null +++ b/dao/src/main/resources/sql/schema-ts.sql @@ -0,0 +1,39 @@ +-- +-- Copyright © 2016-2018 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 ts_kv ( + entity_type varchar(255) NOT NULL, + entity_id varchar(31) NOT NULL, + key varchar(255) NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + CONSTRAINT ts_kv_unq_key UNIQUE (entity_type, entity_id, key, ts) +); + +CREATE TABLE IF NOT EXISTS ts_kv_latest ( + entity_type varchar(255) NOT NULL, + entity_id varchar(31) NOT NULL, + key varchar(255) NOT NULL, + ts bigint NOT NULL, + bool_v boolean, + str_v varchar(10000000), + long_v bigint, + dbl_v double precision, + CONSTRAINT ts_kv_latest_unq_key UNIQUE (entity_type, entity_id, key) +); diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java index 48ba2fae68..a9d2cbc4f9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java @@ -30,7 +30,7 @@ public class JpaDaoTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql"), "sql/drop-all-tables.sql", "sql-test.properties" ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index f10462d77f..55c2f70f1d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -34,7 +34,9 @@ public class NoSqlDaoServiceTestSuite { @ClassRule public static CustomCassandraCQLUnit cassandraUnit = new CustomCassandraCQLUnit( - Arrays.asList(new ClassPathCQLDataSet("cassandra/schema.cql", false, false), + Arrays.asList( + new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), + new ClassPathCQLDataSet("cassandra/schema-entities.cql", false, false), new ClassPathCQLDataSet("cassandra/system-data.cql", false, false), new ClassPathCQLDataSet("cassandra/system-test.cql", false, false)), "cassandra-test.yaml", 30000L); diff --git a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java index 3f65184bfb..9e56d64aba 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/SqlDaoServiceTestSuite.java @@ -30,7 +30,7 @@ public class SqlDaoServiceTestSuite { @ClassRule public static CustomSqlUnit sqlUnit = new CustomSqlUnit( - Arrays.asList("sql/schema.sql", "sql/system-data.sql", "sql/system-test.sql"), + Arrays.asList("sql/schema-ts.sql", "sql/schema-entities.sql", "sql/system-data.sql", "sql/system-test.sql"), "sql/drop-all-tables.sql", "sql-test.properties" ); diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 20cf91c3dd..a61c285136 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -24,6 +24,9 @@ caffeine.specs.devices.maxSize=100000 caffeine.specs.assets.timeToLiveInMinutes=1440 caffeine.specs.assets.maxSize=100000 +caffeine.specs.entityViews.timeToLiveInMinutes=1440 +caffeine.specs.entityViews.maxSize=100000 + caching.specs.devices.timeToLiveInMinutes=1440 caching.specs.devices.maxSize=100000 diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties index e37e228b40..06a92faa01 100644 --- a/dao/src/test/resources/nosql-test.properties +++ b/dao/src/test/resources/nosql-test.properties @@ -1,4 +1,5 @@ -database.type=cassandra +database.entities.type=cassandra +database.ts.type=cassandra cassandra.queue.partitioning=HOURS cassandra.queue.ack.ttl=3600 diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 1f34b98222..3357425fce 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -1,4 +1,5 @@ -database.type=sql +database.ts.type=sql +database.entities.type=sql sql.ts_inserts_executor_type=fixed sql.ts_inserts_fixed_thread_pool_size=10 diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql index 23b6a56a47..b1fb72cc07 100644 --- a/dao/src/test/resources/sql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/drop-all-tables.sql @@ -18,4 +18,5 @@ DROP TABLE IF EXISTS user_credentials; DROP TABLE IF EXISTS widget_type; DROP TABLE IF EXISTS widgets_bundle; DROP TABLE IF EXISTS rule_node; -DROP TABLE IF EXISTS rule_chain; \ No newline at end of file +DROP TABLE IF EXISTS rule_chain; +DROP TABLE IF EXISTS entity_views; diff --git a/docker/k8s/cassandra-setup.yaml b/docker/k8s/cassandra-setup.yaml index 381df77e65..03a2739446 100644 --- a/docker/k8s/cassandra-setup.yaml +++ b/docker/k8s/cassandra-setup.yaml @@ -30,7 +30,9 @@ spec: value: "cassandra-headless" - name : CASSANDRA_PORT value: "9042" - - name : DATABASE_TYPE + - name : DATABASE_ENTITIES_TYPE + value: "cassandra" + - name : DATABASE_TS_TYPE value: "cassandra" - name : CASSANDRA_URL value: "cassandra-headless:9042" diff --git a/docker/k8s/cassandra-upgrade.yaml b/docker/k8s/cassandra-upgrade.yaml index a78136ec41..9276776678 100644 --- a/docker/k8s/cassandra-upgrade.yaml +++ b/docker/k8s/cassandra-upgrade.yaml @@ -30,7 +30,9 @@ spec: value: "cassandra-headless" - name : CASSANDRA_PORT value: "9042" - - name : DATABASE_TYPE + - name : DATABASE_ENTITIES_TYPE + value: "cassandra" + - name : DATABASE_TS_TYPE value: "cassandra" - name : CASSANDRA_URL value: "cassandra-headless:9042" diff --git a/docker/k8s/tb.yaml b/docker/k8s/tb.yaml index f38e1f12b1..741bbe0f26 100644 --- a/docker/k8s/tb.yaml +++ b/docker/k8s/tb.yaml @@ -120,7 +120,12 @@ spec: configMapKeyRef: name: tb-config key: cassandra.url - - name: DATABASE_TYPE + - name: DATABASE_ENTITIES_TYPE + valueFrom: + configMapKeyRef: + name: tb-config + key: database.type + - name: DATABASE_TS_TYPE valueFrom: configMapKeyRef: name: tb-config diff --git a/docker/tb.env b/docker/tb.env index 76afc29962..bc92de5c8b 100644 --- a/docker/tb.env +++ b/docker/tb.env @@ -8,7 +8,8 @@ COAP_BIND_PORT=5683 ZOOKEEPER_URL=zk:2181 # type of database to use: sql[DEFAULT] or cassandra -DATABASE_TYPE=sql +DATABASE_TS_TYPE=sql +DATABASE_ENTITIES_TYPE=sql # cassandra db config CASSANDRA_URL=cassandra:9042 diff --git a/docker/tb/run-application.sh b/docker/tb/run-application.sh index a2a1e2beba..e6b59f9aee 100755 --- a/docker/tb/run-application.sh +++ b/docker/tb/run-application.sh @@ -23,7 +23,7 @@ printenv | awk -F "=" '{print "export " $1 "='\''" $2 "'\''"}' >> /usr/share/thi cat /usr/share/thingsboard/conf/thingsboard.conf -if [ "$DATABASE_TYPE" == "cassandra" ]; then +if [ "$DATABASE_ENTITIES_TYPE" == "cassandra" ]; then until nmap $CASSANDRA_HOST -p $CASSANDRA_PORT | grep "$CASSANDRA_PORT/tcp open\|filtered" do echo "Wait for cassandra db to start..." @@ -31,7 +31,7 @@ if [ "$DATABASE_TYPE" == "cassandra" ]; then done fi -if [ "$DATABASE_TYPE" == "sql" ]; then +if [ "$DATABASE_ENTITIES_TYPE" == "sql" ]; then if [ "$SPRING_DRIVER_CLASS_NAME" == "org.postgresql.Driver" ]; then until nmap $POSTGRES_HOST -p $POSTGRES_PORT | grep "$POSTGRES_PORT/tcp open" do 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 e7ef0dd40f..f9d3c64211 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 @@ -26,6 +26,7 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -83,6 +84,8 @@ public interface TbContext { RelationService getRelationService(); + EntityViewService getEntityViewService(); + ListeningExecutor getJsExecutor(); ListeningExecutor getMailExecutor(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java new file mode 100644 index 0000000000..40e00ec009 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2018 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.action; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.DonAsynchron; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; +import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "copy attributes", + configClazz = EmptyNodeConfiguration.class, + nodeDescription = "Copy attributes from asset/device to entity view and changes message originator to related entity view", + nodeDetails = "Copy attributes from asset/device to related entity view according to entity view configuration. \n " + + "Copy will be done only for attributes that are between start and end dates and according to attribute keys configuration. \n" + + "Changes message originator to related entity view and produces new messages according to count of updated entity views", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbNodeEmptyConfig", + icon = "content_copy" +) +public class TbCopyAttributesToEntityViewNode implements TbNode { + + EmptyNodeConfiguration config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { + if (!msg.getMetaData().getData().isEmpty()) { + long now = System.currentTimeMillis(); + String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ? + DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope"); + + ListenableFuture> entityViewsFuture = + ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator()); + + DonAsynchron.withCallback(entityViewsFuture, + entityViews -> { + for (EntityView entityView : entityViews) { + long startTime = entityView.getStartTimeMs(); + long endTime = entityView.getEndTimeMs(); + if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) { + Set attributes = + JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())).getAttributes(); + List filteredAttributes = + attributes.stream() + .filter(attr -> { + switch (scope) { + case DataConstants.CLIENT_SCOPE: + if (entityView.getKeys().getAttributes().getCs().isEmpty()) { + return true; + } + return entityView.getKeys().getAttributes().getCs().contains(attr.getKey()); + case DataConstants.SERVER_SCOPE: + if (entityView.getKeys().getAttributes().getSs().isEmpty()) { + return true; + } + return entityView.getKeys().getAttributes().getSs().contains(attr.getKey()); + case DataConstants.SHARED_SCOPE: + if (entityView.getKeys().getAttributes().getSh().isEmpty()) { + return true; + } + return entityView.getKeys().getAttributes().getSh().contains(attr.getKey()); + } + return false; + }).collect(Collectors.toList()); + + ctx.getTelemetryService().saveAndNotify(entityView.getId(), scope, filteredAttributes, + new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + TbMsg updMsg = ctx.transformMsg(msg, msg.getType(), entityView.getId(), msg.getMetaData(), msg.getData()); + ctx.tellNext(updMsg, SUCCESS); + } + + @Override + public void onFailure(Throwable t) { + ctx.tellFailure(msg, t); + } + }); + } + } + }, + t -> ctx.tellFailure(msg, t)); + } else { + ctx.tellNext(msg, FAILURE); + } + } + + @Override + public void destroy() { + + } +} diff --git a/ui/src/app/api/entity-view.service.js b/ui/src/app/api/entity-view.service.js new file mode 100644 index 0000000000..e34d3a4aee --- /dev/null +++ b/ui/src/app/api/entity-view.service.js @@ -0,0 +1,238 @@ +/* + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import thingsboardTypes from '../common/types.constant'; + +export default angular.module('thingsboard.api.entityView', [thingsboardTypes]) + .factory('entityViewService', EntityViewService) + .name; + +/*@ngInject*/ +function EntityViewService($http, $q, $window, userService, attributeService, customerService, types) { + + var service = { + assignEntityViewToCustomer: assignEntityViewToCustomer, + deleteEntityView: deleteEntityView, + getCustomerEntityViews: getCustomerEntityViews, + getEntityView: getEntityView, + getEntityViews: getEntityViews, + getTenantEntityViews: getTenantEntityViews, + saveEntityView: saveEntityView, + unassignEntityViewFromCustomer: unassignEntityViewFromCustomer, + getEntityViewAttributes: getEntityViewAttributes, + subscribeForEntityViewAttributes: subscribeForEntityViewAttributes, + unsubscribeForEntityViewAttributes: unsubscribeForEntityViewAttributes, + findByQuery: findByQuery, + getEntityViewTypes: getEntityViewTypes + } + + return service; + + function getTenantEntityViews(pageLink, applyCustomersInfo, config, type) { + var deferred = $q.defer(); + var url = '/api/tenant/entityViews?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + if (angular.isDefined(type) && type.length) { + url += '&type=' + type; + } + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomersInfo(response.data.data).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getCustomerEntityViews(customerId, pageLink, applyCustomersInfo, config, type) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/entityViews?limit=' + pageLink.limit; + if (angular.isDefined(pageLink.textSearch)) { + url += '&textSearch=' + pageLink.textSearch; + } + if (angular.isDefined(pageLink.idOffset)) { + url += '&idOffset=' + pageLink.idOffset; + } + if (angular.isDefined(pageLink.textOffset)) { + url += '&textOffset=' + pageLink.textOffset; + } + if (angular.isDefined(type) && type.length) { + url += '&type=' + type; + } + $http.get(url, config).then(function success(response) { + if (applyCustomersInfo) { + customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( + function success(data) { + response.data.data = data; + deferred.resolve(response.data); + }, + function fail() { + deferred.reject(); + } + ); + } else { + deferred.resolve(response.data); + } + }, function fail() { + deferred.reject(); + }); + + return deferred.promise; + } + + function getEntityView(entityViewId, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/entityView/' + entityViewId; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function getEntityViews(entityViewIds, config) { + var deferred = $q.defer(); + var ids = ''; + for (var i=0;i0) { + ids += ','; + } + ids += entityViewIds[i]; + } + var url = '/api/entityViews?entityViewIds=' + ids; + $http.get(url, config).then(function success(response) { + var entityViews = response.data; + entityViews.sort(function (entityView1, entityView2) { + var id1 = entityView1.id.id; + var id2 = entityView2.id.id; + var index1 = entityViewIds.indexOf(id1); + var index2 = entityViewIds.indexOf(id2); + return index1 - index2; + }); + deferred.resolve(entityViews); + }, function fail(response) { + deferred.reject(response.data); + }); + return deferred.promise; + } + + function saveEntityView(entityView) { + var deferred = $q.defer(); + var url = '/api/entityView'; + + $http.post(url, entityView).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function deleteEntityView(entityViewId) { + var deferred = $q.defer(); + var url = '/api/entityView/' + entityViewId; + $http.delete(url).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function assignEntityViewToCustomer(customerId, entityViewId) { + var deferred = $q.defer(); + var url = '/api/customer/' + customerId + '/entityView/' + entityViewId; + $http.post(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function unassignEntityViewFromCustomer(entityViewId) { + var deferred = $q.defer(); + var url = '/api/customer/entityView/' + entityViewId; + $http.delete(url).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getEntityViewAttributes(entityViewId, attributeScope, query, successCallback, config) { + return attributeService.getEntityAttributes(types.entityType.entityView, entityViewId, attributeScope, query, successCallback, config); + } + + function subscribeForEntityViewAttributes(entityViewId, attributeScope) { + return attributeService.subscribeForEntityAttributes(types.entityType.entityView, entityViewId, attributeScope); + } + + function unsubscribeForEntityViewAttributes(subscriptionId) { + attributeService.unsubscribeForEntityAttributes(subscriptionId); + } + + function findByQuery(query, ignoreErrors, config) { + var deferred = $q.defer(); + var url = '/api/entityViews'; + if (!config) { + config = {}; + } + config = Object.assign(config, { ignoreErrors: ignoreErrors }); + $http.post(url, query, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getEntityViewTypes(config) { + var deferred = $q.defer(); + var url = '/api/entityView/types'; + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + +} diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index 2e29238dfe..ce4dc42dcd 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -20,8 +20,9 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes]) .name; /*@ngInject*/ -function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, - assetService, tenantService, customerService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) { +function EntityService($http, $q, $filter, $translate, $log, userService, deviceService, assetService, tenantService, + customerService, ruleChainService, dashboardService, entityRelationService, attributeService, + entityViewService, types, utils) { var service = { getEntity: getEntity, getEntities: getEntities, @@ -54,6 +55,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device case types.entityType.asset: promise = assetService.getAsset(entityId, true, config); break; + case types.entityType.entityView: + promise = entityViewService.getEntityView(entityId, true, config); + break; case types.entityType.tenant: promise = tenantService.getTenant(entityId, config); break; @@ -239,6 +243,13 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device promise = assetService.getTenantAssets(pageLink, false, config, subType); } break; + case types.entityType.entityView: + if (user.authority === 'CUSTOMER_USER') { + promise = entityViewService.getCustomerEntityViews(customerId, pageLink, false, config, subType); + } else { + promise = entityViewService.getTenantEntityViews(pageLink, false, config, subType); + } + break; case types.entityType.tenant: if (user.authority === 'TENANT_ADMIN') { promise = getSingleTenantByPageLinkPromise(pageLink, config); @@ -725,6 +736,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device case 'TENANT_ADMIN': entityTypes.device = types.entityType.device; entityTypes.asset = types.entityType.asset; + entityTypes.entityView = types.entityType.entityView; entityTypes.tenant = types.entityType.tenant; entityTypes.customer = types.entityType.customer; entityTypes.dashboard = types.entityType.dashboard; @@ -735,6 +747,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device case 'CUSTOMER_USER': entityTypes.device = types.entityType.device; entityTypes.asset = types.entityType.asset; + entityTypes.entityView = types.entityType.entityView; entityTypes.customer = types.entityType.customer; entityTypes.dashboard = types.entityType.dashboard; if (useAliasEntityTypes) { diff --git a/ui/src/app/app.js b/ui/src/app/app.js index c8cdeb02b0..a3a179eb12 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -67,6 +67,7 @@ import thingsboardClipboard from './services/clipboard.service'; import thingsboardHome from './layout'; import thingsboardApiLogin from './api/login.service'; import thingsboardApiDevice from './api/device.service'; +import thingsboardApiEntityView from './api/entity-view.service'; import thingsboardApiUser from './api/user.service'; import thingsboardApiEntityRelation from './api/entity-relation.service'; import thingsboardApiAsset from './api/asset.service'; @@ -133,6 +134,7 @@ angular.module('thingsboard', [ thingsboardHome, thingsboardApiLogin, thingsboardApiDevice, + thingsboardApiEntityView, thingsboardApiUser, thingsboardApiEntityRelation, thingsboardApiAsset, diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 1e34577119..15da584968 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -327,7 +327,8 @@ export default angular.module('thingsboard.types', []) dashboard: "DASHBOARD", alarm: "ALARM", rulechain: "RULE_CHAIN", - rulenode: "RULE_NODE" + rulenode: "RULE_NODE", + entityView: "ENTITY_VIEW" }, aliasEntityType: { current_customer: "CURRENT_CUSTOMER" @@ -345,6 +346,12 @@ export default angular.module('thingsboard.types', []) list: 'entity.list-of-assets', nameStartsWith: 'entity.asset-name-starts-with' }, + "ENTITY_VIEW": { + type: 'entity.type-entity-view', + typePlural: 'entity.type-entity-views', + list: 'entity.list-of-entity-views', + nameStartsWith: 'entity.entity-view-name-starts-with' + }, "TENANT": { type: 'entity.type-tenant', typePlural: 'entity.type-tenants', diff --git a/ui/src/app/entity-view/add-entity-view.tpl.html b/ui/src/app/entity-view/add-entity-view.tpl.html new file mode 100644 index 0000000000..48a1788ed0 --- /dev/null +++ b/ui/src/app/entity-view/add-entity-view.tpl.html @@ -0,0 +1,45 @@ + + +
+ +
+

entity-view.add

+ +
+ + + +
+
+ + + +
+ +
+
+ + + + {{ 'action.add' | translate }} + + {{ 'action.cancel' | translate }} + +
+
\ No newline at end of file diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.controller.js b/ui/src/app/entity-view/add-entity-views-to-customer.controller.js new file mode 100644 index 0000000000..8e39546ffd --- /dev/null +++ b/ui/src/app/entity-view/add-entity-views-to-customer.controller.js @@ -0,0 +1,123 @@ +/* + * Copyright © 2016-2018 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. + */ +/*@ngInject*/ +export default function AddEntityViewsToCustomerController(entityViewService, $mdDialog, $q, customerId, entityViews) { + + var vm = this; + + vm.entityViews = entityViews; + vm.searchText = ''; + + vm.assign = assign; + vm.cancel = cancel; + vm.hasData = hasData; + vm.noData = noData; + vm.searchEntityViewTextUpdated = searchEntityViewTextUpdated; + vm.toggleEntityViewSelection = toggleEntityViewSelection; + + vm.theEntityViews = { + getItemAtIndex: function (index) { + if (index > vm.entityViews.data.length) { + vm.theEntityViews.fetchMoreItems_(index); + return null; + } + var item = vm.entityViews.data[index]; + if (item) { + item.indexNumber = index + 1; + } + return item; + }, + + getLength: function () { + if (vm.entityViews.hasNext) { + return vm.entityViews.data.length + vm.entityViews.nextPageLink.limit; + } else { + return vm.entityViews.data.length; + } + }, + + fetchMoreItems_: function () { + if (vm.entityViews.hasNext && !vm.entityViews.pending) { + vm.entityViews.pending = true; + entityViewService.getTenantEntityViews(vm.entityViews.nextPageLink, false).then( + function success(entityViews) { + vm.entityViews.data = vm.entityViews.data.concat(entityViews.data); + vm.entityViews.nextPageLink = entityViews.nextPageLink; + vm.entityViews.hasNext = entityViews.hasNext; + if (vm.entityViews.hasNext) { + vm.entityViews.nextPageLink.limit = vm.entityViews.pageSize; + } + vm.entityViews.pending = false; + }, + function fail() { + vm.entityViews.hasNext = false; + vm.entityViews.pending = false; + }); + } + } + }; + + function cancel () { + $mdDialog.cancel(); + } + + function assign() { + var tasks = []; + for (var entityViewId in vm.entityViews.selections) { + tasks.push(entityViewService.assignEntityViewToCustomer(customerId, entityViewId)); + } + $q.all(tasks).then(function () { + $mdDialog.hide(); + }); + } + + function noData() { + return vm.entityViews.data.length == 0 && !vm.entityViews.hasNext; + } + + function hasData() { + return vm.entityViews.data.length > 0; + } + + function toggleEntityViewSelection($event, entityView) { + $event.stopPropagation(); + var selected = angular.isDefined(entityView.selected) && entityView.selected; + entityView.selected = !selected; + if (entityView.selected) { + vm.entityViews.selections[entityView.id.id] = true; + vm.entityViews.selectedCount++; + } else { + delete vm.entityViews.selections[entityView.id.id]; + vm.entityViews.selectedCount--; + } + } + + function searchEntityViewTextUpdated() { + vm.entityViews = { + pageSize: vm.entityViews.pageSize, + data: [], + nextPageLink: { + limit: vm.entityViews.pageSize, + textSearch: vm.searchText + }, + selections: {}, + selectedCount: 0, + hasNext: true, + pending: false + }; + } + +} diff --git a/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html b/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html new file mode 100644 index 0000000000..1149a1d8be --- /dev/null +++ b/ui/src/app/entity-view/add-entity-views-to-customer.tpl.html @@ -0,0 +1,77 @@ + + +
+ +
+

entity-view.assign-entity-view-to-customer

+ + + + +
+
+ + + +
+
+ entity-view.assign-entity-view-to-customer-text + + + + search + + + +
+ entity-view.no-entity-views-text + + + + + {{ entityView.name }} + + + +
+
+
+
+ + + + {{ 'action.assign' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
\ No newline at end of file diff --git a/ui/src/app/entity-view/assign-to-customer.controller.js b/ui/src/app/entity-view/assign-to-customer.controller.js new file mode 100644 index 0000000000..3e09ae6e4f --- /dev/null +++ b/ui/src/app/entity-view/assign-to-customer.controller.js @@ -0,0 +1,123 @@ +/* + * Copyright © 2016-2018 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. + */ +/*@ngInject*/ +export default function AssignEntityViewToCustomerController(customerService, entityViewService, $mdDialog, $q, entityViewIds, customers) { + + var vm = this; + + vm.customers = customers; + vm.searchText = ''; + + vm.assign = assign; + vm.cancel = cancel; + vm.isCustomerSelected = isCustomerSelected; + vm.hasData = hasData; + vm.noData = noData; + vm.searchCustomerTextUpdated = searchCustomerTextUpdated; + vm.toggleCustomerSelection = toggleCustomerSelection; + + vm.theCustomers = { + getItemAtIndex: function (index) { + if (index > vm.customers.data.length) { + vm.theCustomers.fetchMoreItems_(index); + return null; + } + var item = vm.customers.data[index]; + if (item) { + item.indexNumber = index + 1; + } + return item; + }, + + getLength: function () { + if (vm.customers.hasNext) { + return vm.customers.data.length + vm.customers.nextPageLink.limit; + } else { + return vm.customers.data.length; + } + }, + + fetchMoreItems_: function () { + if (vm.customers.hasNext && !vm.customers.pending) { + vm.customers.pending = true; + customerService.getCustomers(vm.customers.nextPageLink).then( + function success(customers) { + vm.customers.data = vm.customers.data.concat(customers.data); + vm.customers.nextPageLink = customers.nextPageLink; + vm.customers.hasNext = customers.hasNext; + if (vm.customers.hasNext) { + vm.customers.nextPageLink.limit = vm.customers.pageSize; + } + vm.customers.pending = false; + }, + function fail() { + vm.customers.hasNext = false; + vm.customers.pending = false; + }); + } + } + }; + + function cancel() { + $mdDialog.cancel(); + } + + function assign() { + var tasks = []; + for (var i=0; i < entityViewIds.length;i++) { + tasks.push(entityViewService.assignEntityViewToCustomer(vm.customers.selection.id.id, entityViewIds[i])); + } + $q.all(tasks).then(function () { + $mdDialog.hide(); + }); + } + + function noData() { + return vm.customers.data.length == 0 && !vm.customers.hasNext; + } + + function hasData() { + return vm.customers.data.length > 0; + } + + function toggleCustomerSelection($event, customer) { + $event.stopPropagation(); + if (vm.isCustomerSelected(customer)) { + vm.customers.selection = null; + } else { + vm.customers.selection = customer; + } + } + + function isCustomerSelected(customer) { + return vm.customers.selection != null && customer && + customer.id.id === vm.customers.selection.id.id; + } + + function searchCustomerTextUpdated() { + vm.customers = { + pageSize: vm.customers.pageSize, + data: [], + nextPageLink: { + limit: vm.customers.pageSize, + textSearch: vm.searchText + }, + selection: null, + hasNext: true, + pending: false + }; + } +} diff --git a/ui/src/app/entity-view/assign-to-customer.tpl.html b/ui/src/app/entity-view/assign-to-customer.tpl.html new file mode 100644 index 0000000000..7c1fa2540e --- /dev/null +++ b/ui/src/app/entity-view/assign-to-customer.tpl.html @@ -0,0 +1,76 @@ + + +
+ +
+

entity-view.assign-entity-view-to-customer

+ + + + +
+
+ + + +
+
+ entity-view.assign-to-customer-text + + + + search + + + +
+ customer.no-customers-text + + + + + {{ customer.title }} + + + +
+
+
+
+ + + + {{ 'action.assign' | translate }} + + {{ 'action.cancel' | + translate }} + + +
+
\ No newline at end of file diff --git a/ui/src/app/entity-view/entity-view-card.tpl.html b/ui/src/app/entity-view/entity-view-card.tpl.html new file mode 100644 index 0000000000..1e90928540 --- /dev/null +++ b/ui/src/app/entity-view/entity-view-card.tpl.html @@ -0,0 +1,22 @@ + +
+
{{vm.item.type}}
+
{{vm.item.additionalInfo.description}}
+
{{'entity-view.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
+
diff --git a/ui/src/app/entity-view/entity-view-fieldset.tpl.html b/ui/src/app/entity-view/entity-view-fieldset.tpl.html new file mode 100644 index 0000000000..3894eb9d63 --- /dev/null +++ b/ui/src/app/entity-view/entity-view-fieldset.tpl.html @@ -0,0 +1,119 @@ + +{{ 'entity-view.make-public' | translate }} +{{ 'entity-view.assign-to-customer' | translate }} +{{'entity-view.unassign-from-customer' | translate }} +{{ 'entity-view.delete' | translate }} + +
+ + + entity-view.copyId + +
+ + + + + + +
+ + + +
+
entity-view.name-required
+
+
+ + + + + + +
+ + + + + + + + + + + + +
+
+
+ + +
+
+ + +
+
+
+
diff --git a/ui/src/app/entity-view/entity-view.controller.js b/ui/src/app/entity-view/entity-view.controller.js new file mode 100644 index 0000000000..2c8c8ea036 --- /dev/null +++ b/ui/src/app/entity-view/entity-view.controller.js @@ -0,0 +1,483 @@ +/* + * Copyright © 2016-2018 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import addEntityViewTemplate from './add-entity-view.tpl.html'; +import entityViewCard from './entity-view-card.tpl.html'; +import assignToCustomerTemplate from './assign-to-customer.tpl.html'; +import addEntityViewsToCustomerTemplate from './add-entity-views-to-customer.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export function EntityViewCardController(types) { + + var vm = this; + + vm.types = types; + + vm.isAssignedToCustomer = function() { + if (vm.item && vm.item.customerId && vm.parentCtl.entityViewsScope === 'tenant' && + vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } + + vm.isPublic = function() { + if (vm.item && vm.item.assignedCustomer && vm.parentCtl.entityViewsScope === 'tenant' && vm.item.assignedCustomer.isPublic) { + return true; + } + return false; + } +} + + +/*@ngInject*/ +export function EntityViewController($rootScope, userService, entityViewService, customerService, $state, $stateParams, + $document, $mdDialog, $q, $translate, types) { + + var customerId = $stateParams.customerId; + + var entityViewActionsList = []; + + var entityViewGroupActionsList = []; + + var vm = this; + + vm.types = types; + + vm.entityViewGridConfig = { + deleteItemTitleFunc: deleteEntityViewTitle, + deleteItemContentFunc: deleteEntityViewText, + deleteItemsTitleFunc: deleteEntityViewsTitle, + deleteItemsActionTitleFunc: deleteEntityViewsActionTitle, + deleteItemsContentFunc: deleteEntityViewsText, + + saveItemFunc: saveEntityView, + + getItemTitleFunc: getEntityViewTitle, + + itemCardController: 'EntityViewCardController', + itemCardTemplateUrl: entityViewCard, + parentCtl: vm, + + actionsList: entityViewActionsList, + groupActionsList: entityViewGroupActionsList, + + onGridInited: gridInited, + + addItemTemplateUrl: addEntityViewTemplate, + + addItemText: function() { return $translate.instant('entity-view.add-entity-view-text') }, + noItemsText: function() { return $translate.instant('entity-view.no-entity-views-text') }, + itemDetailsText: function() { return $translate.instant('entity-view.entity-view-details') }, + isDetailsReadOnly: isCustomerUser, + isSelectionEnabled: function () { + return !isCustomerUser(); + } + }; + + if (angular.isDefined($stateParams.items) && $stateParams.items !== null) { + vm.entityViewGridConfig.items = $stateParams.items; + } + + if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) { + vm.entityViewGridConfig.topIndex = $stateParams.topIndex; + } + + vm.entityViewsScope = $state.$current.data.entityViewsType; + + vm.assignToCustomer = assignToCustomer; + vm.makePublic = makePublic; + vm.unassignFromCustomer = unassignFromCustomer; + + initController(); + + function initController() { + var fetchEntityViewsFunction = null; + var deleteEntityViewFunction = null; + var refreshEntityViewsParamsFunction = null; + + var user = userService.getCurrentUser(); + + if (user.authority === 'CUSTOMER_USER') { + vm.entityViewsScope = 'customer_user'; + customerId = user.customerId; + } + if (customerId) { + vm.customerEntityViewsTitle = $translate.instant('customer.entity-views'); + customerService.getShortCustomerInfo(customerId).then( + function success(info) { + if (info.isPublic) { + vm.customerEntityViewsTitle = $translate.instant('customer.public-entity-views'); + } + } + ); + } + + if (vm.entityViewsScope === 'tenant') { + fetchEntityViewsFunction = function (pageLink, entityViewType) { + return entityViewService.getTenantEntityViews(pageLink, true, null, entityViewType); + }; + deleteEntityViewFunction = function (entityViewId) { + return entityViewService.deleteEntityView(entityViewId); + }; + refreshEntityViewsParamsFunction = function() { + return {"topIndex": vm.topIndex}; + }; + + entityViewActionsList.push( + { + onAction: function ($event, item) { + assignToCustomer($event, [ item.id.id ]); + }, + name: function() { return $translate.instant('action.assign') }, + details: function() { return $translate.instant('entity-view.assign-to-customer') }, + icon: "assignment_ind", + isEnabled: function(entityView) { + return entityView && (!entityView.customerId || entityView.customerId.id === types.id.nullUid); + } + } + ); + + entityViewActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, false); + }, + name: function() { return $translate.instant('action.unassign') }, + details: function() { return $translate.instant('entity-view.unassign-from-customer') }, + icon: "assignment_return", + isEnabled: function(entityView) { + return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && !entityView.assignedCustomer.isPublic; + } + } + ); + + entityViewActionsList.push({ + onAction: function ($event, item) { + unassignFromCustomer($event, item, true); + }, + name: function() { return $translate.instant('action.make-private') }, + details: function() { return $translate.instant('entity-view.make-private') }, + icon: "reply", + isEnabled: function(entityView) { + return entityView && entityView.customerId && entityView.customerId.id !== types.id.nullUid && entityView.assignedCustomer.isPublic; + } + }); + + entityViewActionsList.push( + { + onAction: function ($event, item) { + vm.grid.deleteItem($event, item); + }, + name: function() { return $translate.instant('action.delete') }, + details: function() { return $translate.instant('entity-view.delete') }, + icon: "delete" + } + ); + + entityViewGroupActionsList.push( + { + onAction: function ($event, items) { + assignEntityViewsToCustomer($event, items); + }, + name: function() { return $translate.instant('entity-view.assign-entity-views') }, + details: function(selectedCount) { + return $translate.instant('entity-view.assign-entity-views-text', {count: selectedCount}, "messageformat"); + }, + icon: "assignment_ind" + } + ); + + entityViewGroupActionsList.push( + { + onAction: function ($event) { + vm.grid.deleteItems($event); + }, + name: function() { return $translate.instant('entity-view.delete-entity-views') }, + details: deleteEntityViewsActionTitle, + icon: "delete" + } + ); + + + + } else if (vm.entityViewsScope === 'customer' || vm.entityViewsScope === 'customer_user') { + fetchEntityViewsFunction = function (pageLink, entityViewType) { + return entityViewService.getCustomerEntityViews(customerId, pageLink, true, null, entityViewType); + }; + deleteEntityViewFunction = function (entityViewId) { + return entityViewService.unassignEntityViewFromCustomer(entityViewId); + }; + refreshEntityViewsParamsFunction = function () { + return {"customerId": customerId, "topIndex": vm.topIndex}; + }; + + if (vm.entityViewsScope === 'customer') { + entityViewActionsList.push( + { + onAction: function ($event, item) { + unassignFromCustomer($event, item, false); + }, + name: function() { return $translate.instant('action.unassign') }, + details: function() { return $translate.instant('entity-view.unassign-from-customer') }, + icon: "assignment_return", + isEnabled: function(entityView) { + return entityView && !entityView.assignedCustomer.isPublic; + } + } + ); + + entityViewGroupActionsList.push( + { + onAction: function ($event, items) { + unassignEntityViewsFromCustomer($event, items); + }, + name: function() { return $translate.instant('entity-view.unassign-entity-views') }, + details: function(selectedCount) { + return $translate.instant('entity-view.unassign-entity-views-action-title', {count: selectedCount}, "messageformat"); + }, + icon: "assignment_return" + } + ); + + vm.entityViewGridConfig.addItemAction = { + onAction: function ($event) { + addEntityViewsToCustomer($event); + }, + name: function() { return $translate.instant('entity-view.assign-entity-views') }, + details: function() { return $translate.instant('entity-view.assign-new-entity-view') }, + icon: "add" + }; + + + } else if (vm.entityViewsScope === 'customer_user') { + vm.entityViewGridConfig.addItemAction = {}; + } + } + + vm.entityViewGridConfig.refreshParamsFunc = refreshEntityViewsParamsFunction; + vm.entityViewGridConfig.fetchItemsFunc = fetchEntityViewsFunction; + vm.entityViewGridConfig.deleteItemFunc = deleteEntityViewFunction; + + } + + function deleteEntityViewTitle(entityView) { + return $translate.instant('entity-view.delete-entity-view-title', {entityViewName: entityView.name}); + } + + function deleteEntityViewText() { + return $translate.instant('entity-view.delete-entity-view-text'); + } + + function deleteEntityViewsTitle(selectedCount) { + return $translate.instant('entity-view.delete-entity-views-title', {count: selectedCount}, 'messageformat'); + } + + function deleteEntityViewsActionTitle(selectedCount) { + return $translate.instant('entity-view.delete-entity-views-action-title', {count: selectedCount}, 'messageformat'); + } + + function deleteEntityViewsText () { + return $translate.instant('entity-view.delete-entity-views-text'); + } + + function gridInited(grid) { + vm.grid = grid; + } + + function getEntityViewTitle(entityView) { + return entityView ? entityView.name : ''; + } + + function saveEntityView(entityView) { + var deferred = $q.defer(); + entityViewService.saveEntityView(entityView).then( + function success(savedEntityView) { + $rootScope.$broadcast('entityViewSaved'); + var entityViews = [ savedEntityView ]; + customerService.applyAssignedCustomersInfo(entityViews).then( + function success(items) { + if (items && items.length == 1) { + deferred.resolve(items[0]); + } else { + deferred.reject(); + } + }, + function fail() { + deferred.reject(); + } + ); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + function isCustomerUser() { + return vm.entityViewsScope === 'customer_user'; + } + + function assignToCustomer($event, entityViewIds) { + if ($event) { + $event.stopPropagation(); + } + var pageSize = 10; + customerService.getCustomers({limit: pageSize, textSearch: ''}).then( + function success(_customers) { + var customers = { + pageSize: pageSize, + data: _customers.data, + nextPageLink: _customers.nextPageLink, + selection: null, + hasNext: _customers.hasNext, + pending: false + }; + if (customers.hasNext) { + customers.nextPageLink.limit = pageSize; + } + $mdDialog.show({ + controller: 'AssignEntityViewToCustomerController', + controllerAs: 'vm', + templateUrl: assignToCustomerTemplate, + locals: {entityViewIds: entityViewIds, customers: customers}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }, function () { + }); + }, + function fail() { + }); + } + + function addEntityViewsToCustomer($event) { + if ($event) { + $event.stopPropagation(); + } + var pageSize = 10; + entityViewService.getTenantEntityViews({limit: pageSize, textSearch: ''}, false).then( + function success(_entityViews) { + var entityViews = { + pageSize: pageSize, + data: _entityViews.data, + nextPageLink: _entityViews.nextPageLink, + selections: {}, + selectedCount: 0, + hasNext: _entityViews.hasNext, + pending: false + }; + if (entityViews.hasNext) { + entityViews.nextPageLink.limit = pageSize; + } + $mdDialog.show({ + controller: 'AddEntityViewsToCustomerController', + controllerAs: 'vm', + templateUrl: addEntityViewsToCustomerTemplate, + locals: {customerId: customerId, entityViews: entityViews}, + parent: angular.element($document[0].body), + fullscreen: true, + targetEvent: $event + }).then(function () { + vm.grid.refreshList(); + }, function () { + }); + }, + function fail() { + }); + } + + function assignEntityViewsToCustomer($event, items) { + var entityViewIds = []; + for (var id in items.selections) { + entityViewIds.push(id); + } + assignToCustomer($event, entityViewIds); + } + + function unassignFromCustomer($event, entityView, isPublic) { + if ($event) { + $event.stopPropagation(); + } + var title; + var content; + var label; + if (isPublic) { + title = $translate.instant('entity-view.make-private-entity-view-title', {entityViewName: entityView.name}); + content = $translate.instant('entity-view.make-private-entity-view-text'); + label = $translate.instant('entity-view.make-private'); + } else { + title = $translate.instant('entity-view.unassign-entity-view-title', {entityViewName: entityView.name}); + content = $translate.instant('entity-view.unassign-entity-view-text'); + label = $translate.instant('entity-view.unassign-entity-view'); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(label) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + entityViewService.unassignEntityViewFromCustomer(entityView.id.id).then(function success() { + vm.grid.refreshList(); + }); + }); + } + + function unassignEntityViewsFromCustomer($event, items) { + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('entity-view.unassign-entity-views-title', {count: items.selectedCount}, 'messageformat')) + .htmlContent($translate.instant('entity-view.unassign-entity-views-text')) + .ariaLabel($translate.instant('entity-view.unassign-entity-view')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + var tasks = []; + for (var id in items.selections) { + tasks.push(entityViewService.unassignEntityViewFromCustomer(id)); + } + $q.all(tasks).then(function () { + vm.grid.refreshList(); + }); + }); + } + + function makePublic($event, entityView) { + if ($event) { + $event.stopPropagation(); + } + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title($translate.instant('entity-view.make-public-entity-view-title', {entityViewName: entityView.name})) + .htmlContent($translate.instant('entity-view.make-public-entity-view-text')) + .ariaLabel($translate.instant('entity-view.make-public')) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + $mdDialog.show(confirm).then(function () { + entityViewService.makeEntityViewPublic(entityView.id.id).then(function success() { + vm.grid.refreshList(); + }); + }); + } +} diff --git a/ui/src/app/entity-view/entity-view.directive.js b/ui/src/app/entity-view/entity-view.directive.js new file mode 100644 index 0000000000..e1ae82fa6a --- /dev/null +++ b/ui/src/app/entity-view/entity-view.directive.js @@ -0,0 +1,121 @@ +/* + * Copyright © 2016-2018 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import entityViewFieldsetTemplate from './entity-view-fieldset.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EntityViewDirective($compile, $templateCache, $filter, toast, $translate, $mdConstant, + types, clipboardService, entityViewService, customerService) { + var linker = function (scope, element) { + var template = $templateCache.get(entityViewFieldsetTemplate); + element.html(template); + + scope.types = types; + scope.isAssignedToCustomer = false; + scope.isPublic = false; + scope.assignedCustomer = null; + + scope.allowedEntityTypes = [types.entityType.device, types.entityType.asset]; + + var semicolon = 186; + scope.separatorKeys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.COMMA, semicolon]; + + scope.$watch('entityView', function(newVal) { + if (newVal) { + if (scope.entityView.customerId && scope.entityView.customerId.id !== types.id.nullUid) { + scope.isAssignedToCustomer = true; + customerService.getShortCustomerInfo(scope.entityView.customerId.id).then( + function success(customer) { + scope.assignedCustomer = customer; + scope.isPublic = customer.isPublic; + } + ); + } else { + scope.isAssignedToCustomer = false; + scope.isPublic = false; + scope.assignedCustomer = null; + } + if (scope.entityView.startTimeMs > 0) { + scope.startTimeMs = new Date(scope.entityView.startTimeMs); + } + if (scope.entityView.endTimeMs > 0) { + scope.endTimeMs = new Date(scope.entityView.endTimeMs); + } + if (!scope.entityView.keys) { + scope.entityView.keys = {}; + scope.entityView.keys.timeseries = []; + scope.entityView.keys.attributes = {}; + scope.entityView.keys.attributes.ss = []; + scope.entityView.keys.attributes.cs = []; + scope.entityView.keys.attributes.sh = []; + } + } + }); + + + scope.$watch('startTimeMs', function (newDate) { + if (newDate) { + if (newDate.getTime() > scope.maxStartTimeMs) { + scope.startTimeMs = angular.copy(scope.maxStartTimeMs); + } + updateMinMaxDates(); + } + }); + + scope.$watch('endTimeMs', function (newDate) { + if (newDate) { + if (newDate.getTime() < scope.minEndTimeMs) { + scope.endTimeMs = angular.copy(scope.minEndTimeMs); + } + updateMinMaxDates(); + } + }); + + function updateMinMaxDates() { + if (scope.endTimeMs) { + scope.maxStartTimeMs = angular.copy(new Date(scope.endTimeMs.getTime())); + scope.entityView.endTimeMs = scope.endTimeMs.getTime(); + } + if (scope.startTimeMs) { + scope.minEndTimeMs = angular.copy(new Date(scope.startTimeMs.getTime())); + scope.entityView.startTimeMs = scope.startTimeMs.getTime(); + } + } + + scope.onEntityViewIdCopied = function() { + toast.showSuccess($translate.instant('entity-view.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + }; + + $compile(element.contents())(scope); + } + return { + restrict: "E", + link: linker, + scope: { + entityView: '=', + isEdit: '=', + entityViewScope: '=', + theForm: '=', + onAssignToCustomer: '&', + onMakePublic: '&', + onUnassignFromCustomer: '&', + onDeleteEntityView: '&' + } + }; +} diff --git a/ui/src/app/entity-view/entity-view.routes.js b/ui/src/app/entity-view/entity-view.routes.js new file mode 100644 index 0000000000..c14d430f32 --- /dev/null +++ b/ui/src/app/entity-view/entity-view.routes.js @@ -0,0 +1,72 @@ +/* + * Copyright © 2016-2018 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import entityViewsTemplate from './entity-views.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function EntityViewRoutes($stateProvider, types) { + $stateProvider + .state('home.entityViews', { + url: '/entityViews', + params: {'topIndex': 0}, + module: 'private', + auth: ['TENANT_ADMIN', 'CUSTOMER_USER'], + views: { + "content@home": { + templateUrl: entityViewsTemplate, + controller: 'EntityViewController', + controllerAs: 'vm' + } + }, + data: { + entityViewsType: 'tenant', + searchEnabled: true, + searchByEntitySubtype: true, + searchEntityType: types.entityType.entityView, + pageTitle: 'entity-view.entity-views' + }, + ncyBreadcrumb: { + label: '{"icon": "view_stream", "label": "entity-view.entity-views"}' + } + }) + .state('home.customers.entityViews', { + url: '/:customerId/entityViews', + params: {'topIndex': 0}, + module: 'private', + auth: ['TENANT_ADMIN'], + views: { + "content@home": { + templateUrl: entityViewsTemplate, + controllerAs: 'vm', + controller: 'EntityViewController' + } + }, + data: { + entityViewsType: 'customer', + searchEnabled: true, + searchByEntitySubtype: true, + searchEntityType: types.entityType.entityView, + pageTitle: 'customer.entity-views' + }, + ncyBreadcrumb: { + label: '{"icon": "view_stream", "label": "{{ vm.customerEntityViewsTitle }}", "translate": "false"}' + } + }); + +} diff --git a/ui/src/app/entity-view/entity-views.tpl.html b/ui/src/app/entity-view/entity-views.tpl.html new file mode 100644 index 0000000000..50a0adb5b1 --- /dev/null +++ b/ui/src/app/entity-view/entity-views.tpl.html @@ -0,0 +1,75 @@ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/ui/src/app/entity-view/index.js b/ui/src/app/entity-view/index.js new file mode 100644 index 0000000000..69f669e4cf --- /dev/null +++ b/ui/src/app/entity-view/index.js @@ -0,0 +1,41 @@ +/* + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import uiRouter from 'angular-ui-router'; +import thingsboardGrid from '../components/grid.directive'; +import thingsboardApiUser from '../api/user.service'; +import thingsboardApiEntityView from '../api/entity-view.service'; +import thingsboardApiCustomer from '../api/customer.service'; + +import EntityViewRoutes from './entity-view.routes'; +import {EntityViewController, EntityViewCardController} from './entity-view.controller'; +import AssignEntityViewToCustomerController from './assign-to-customer.controller'; +import AddEntityViewsToCustomerController from './add-entity-views-to-customer.controller'; +import EntityViewDirective from './entity-view.directive'; + +export default angular.module('thingsboard.entityView', [ + uiRouter, + thingsboardGrid, + thingsboardApiUser, + thingsboardApiEntityView, + thingsboardApiCustomer +]) + .config(EntityViewRoutes) + .controller('EntityViewController', EntityViewController) + .controller('EntityViewCardController', EntityViewCardController) + .controller('AssignEntityViewToCustomerController', AssignEntityViewToCustomerController) + .controller('AddEntityViewsToCustomerController', AddEntityViewsToCustomerController) + .directive('tbEntityView', EntityViewDirective) + .name; diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js index 00618541ce..3a7c164885 100644 --- a/ui/src/app/entity/attribute/attribute-table.directive.js +++ b/ui/src/app/entity/attribute/attribute-table.directive.js @@ -54,7 +54,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS scope.entityType = attrs.entityType; - if (scope.entityType === types.entityType.device) { + if (scope.entityType === types.entityType.device || scope.entityType === types.entityType.entityView) { scope.attributeScopes = types.attributesScope; scope.attributeScopeSelectionReadonly = false; } else { diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js index e46c614c33..8c1fed76f1 100644 --- a/ui/src/app/entity/entity-autocomplete.directive.js +++ b/ui/src/app/entity/entity-autocomplete.directive.js @@ -131,6 +131,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter scope.noEntitiesMatchingText = 'device.no-devices-matching'; scope.entityRequiredText = 'device.device-required'; break; + case types.entityType.entityView: + scope.selectEntityText = 'entity-view.select-entity-view'; + scope.entityText = 'entity-view.entity-view'; + scope.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; + scope.entityRequiredText = 'entity-view.entity-view-required'; + break; case types.entityType.rulechain: scope.selectEntityText = 'rulechain.select-rulechain'; scope.entityText = 'rulechain.rulechain'; diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 8f2958d9e3..192400a646 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -48,6 +48,7 @@ import thingsboardAdmin from '../admin'; import thingsboardProfile from '../profile'; import thingsboardAsset from '../asset'; import thingsboardDevice from '../device'; +import thingsboardEntityView from '../entity-view'; import thingsboardWidgetLibrary from '../widget'; import thingsboardDashboard from '../dashboard'; import thingsboardRuleChain from '../rulechain'; @@ -79,6 +80,7 @@ export default angular.module('thingsboard.home', [ thingsboardProfile, thingsboardAsset, thingsboardDevice, + thingsboardEntityView, thingsboardWidgetLibrary, thingsboardDashboard, thingsboardRuleChain, diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 2caf01331b..481d677955 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -343,10 +343,12 @@ "dashboard": "Customer Dashboard", "dashboards": "Customer Dashboards", "devices": "Customer Devices", + "entity-views": "Customer Entity Views", "assets": "Customer Assets", "public-dashboards": "Public Dashboards", "public-devices": "Public Devices", "public-assets": "Public Assets", + "public-entity-views": "Public Entity Views", "add": "Add Customer", "delete": "Delete customer", "manage-customer-users": "Manage customer users", @@ -711,6 +713,10 @@ "type-assets": "Assets", "list-of-assets": "{ count, plural, 1 {One asset} other {List of # assets} }", "asset-name-starts-with": "Assets whose names start with '{{prefix}}'", + "type-entity-view": "Entity View", + "type-entity-views": "Entity Views", + "list-of-entity-views": "{ count, plural, 1 {One entity view} other {List of # entity views} }", + "entity-view-name-starts-with": "Entity Views whose names start with '{{prefix}}'", "type-rule": "Rule", "type-rules": "Rules", "list-of-rules": "{ count, plural, 1 {One rule} other {List of # rules} }", @@ -756,6 +762,85 @@ "no-data": "No data to display", "columns-to-display": "Columns to Display" }, + "entity-view": { + "entity-view": "Entity View", + "entity-views": "Entity Views", + "management": "Entity View management", + "view-entity-views": "View Entity Views", + "entity-view-alias": "Entity View alias", + "aliases": "Entity View aliases", + "no-alias-matching": "'{{alias}}' not found.", + "no-aliases-found": "No aliases found.", + "no-key-matching": "'{{key}}' not found.", + "no-keys-found": "No keys found.", + "create-new-alias": "Create a new one!", + "create-new-key": "Create a new one!", + "duplicate-alias-error": "Duplicate alias found '{{alias}}'.
Entity View aliases must be unique whithin the dashboard.", + "configure-alias": "Configure '{{alias}}' alias", + "no-entity-views-matching": "No entity views matching '{{entity}}' were found.", + "alias": "Alias", + "alias-required": "Entity View alias is required.", + "remove-alias": "Remove entity view alias", + "add-alias": "Add entity view alias", + "name-starts-with": "Entity View name starts with", + "entity-view-list": "Entity View list", + "use-entity-view-name-filter": "Use filter", + "entity-view-list-empty": "No entity views selected.", + "entity-view-name-filter-required": "Entity view name filter is required.", + "entity-view-name-filter-no-entity-view-matched": "No entity views starting with '{{entityView}}' were found.", + "add": "Add Entity View", + "assign-to-customer": "Assign to customer", + "assign-entity-view-to-customer": "Assign Entity View(s) To Customer", + "assign-entity-view-to-customer-text": "Please select the entity views to assign to the customer", + "no-entity-views-text": "No entity views found", + "assign-to-customer-text": "Please select the customer to assign the entity view(s)", + "entity-view-details": "Entity view details", + "add-entity-view-text": "Add new entity view", + "delete": "Delete entity view", + "assign-entity-views": "Assign entity views", + "assign-entity-views-text": "Assign { count, plural, 1 {1 entityView} other {# entityViews} } to customer", + "delete-entity-views": "Delete entity views", + "unassign-from-customer": "Unassign from customer", + "unassign-entity-views": "Unassign entity views", + "unassign-entity-views-action-title": "Unassign { count, plural, 1 {1 entityView} other {# entityViews} } from customer", + "assign-new-entity-view": "Assign new entity view", + "delete-entity-view-title": "Are you sure you want to delete the entity view '{{entityViewName}}'?", + "delete-entity-view-text": "Be careful, after the confirmation the entity view and all related data will become unrecoverable.", + "delete-entity-views-title": "Are you sure you want to entity view { count, plural, 1 {1 entityView} other {# entityViews} }?", + "delete-entity-views-action-title": "Delete { count, plural, 1 {1 entityView} other {# entityViews} }", + "delete-entity-views-text": "Be careful, after the confirmation all selected entity views will be removed and all related data will become unrecoverable.", + "unassign-entity-view-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", + "unassign-entity-view-text": "After the confirmation the entity view will be unassigned and won't be accessible by the customer.", + "unassign-entity-view": "Unassign entity view", + "unassign-entity-views-title": "Are you sure you want to unassign { count, plural, 1 {1 entityView} other {# entityViews} }?", + "unassign-entity-views-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the customer.", + "entity-view-type": "Entity View type", + "entity-view-type-required": "Entity View type is required.", + "select-entity-view-type": "Select entity view type", + "enter-entity-view-type": "Enter entity view type", + "any-entity-view": "Any entity view", + "no-entity-view-types-matching": "No entity view types matching '{{entitySubtype}}' were found.", + "entity-view-type-list-empty": "No entity view types selected.", + "entity-view-types": "Entity View types", + "name": "Name", + "name-required": "Name is required.", + "description": "Description", + "events": "Events", + "details": "Details", + "copyId": "Copy entity view Id", + "assignedToCustomer": "Assigned to customer", + "unable-entity-view-device-alias-title": "Unable to delete entity view alias", + "unable-entity-view-device-alias-text": "Device alias '{{entityViewAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", + "select-entity-view": "Select entity view", + "make-public": "Make entity view public", + "start-ts": "Start time", + "end-ts": "End time", + "date-limits": "Date limits", + "client-attributes": "Client attributes", + "shared-attributes": "Shared attributes", + "server-attributes": "Server attributes", + "latest-timeseries": "Latest timeseries" + }, "event": { "event-type": "Event type", "type-error": "Error", diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js index 1c33d1f18c..869a25d860 100644 --- a/ui/src/app/services/menu.service.js +++ b/ui/src/app/services/menu.service.js @@ -167,6 +167,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.devices', icon: 'devices_other' }, + { + name: 'entity-view.entity-views', + type: 'link', + state: 'home.entityViews', + icon: 'view_stream' + }, { name: 'widget.widget-library', type: 'link', @@ -227,6 +233,16 @@ function Menu(userService, $state, $rootScope) { } ] }, + { + name: 'entity-view.management', + places: [ + { + name: 'entity-view.entity-views', + icon: 'view_stream', + state: 'home.entityViews' + } + ] + }, { name: 'dashboard.management', places: [ @@ -273,6 +289,12 @@ function Menu(userService, $state, $rootScope) { state: 'home.devices', icon: 'devices_other' }, + { + name: 'entity-view.entity-views', + type: 'link', + state: 'home.entityViews', + icon: 'view_stream' + }, { name: 'dashboard.dashboards', type: 'link', @@ -301,16 +323,26 @@ function Menu(userService, $state, $rootScope) { } ] }, - { - name: 'dashboard.view-dashboards', - places: [ - { - name: 'dashboard.dashboards', - icon: 'dashboard', - state: 'home.dashboards' - } - ] - }]; + { + name: 'entity-view.management', + places: [ + { + name: 'entity-view.entity-views', + icon: 'view_stream', + state: 'home.entityViews' + } + ] + }, + { + name: 'dashboard.view-dashboards', + places: [ + { + name: 'dashboard.dashboards', + icon: 'dashboard', + state: 'home.dashboards' + } + ] + }]; } } }