From 5a8510c5c2c0a0e4710f30b536f1e2c1f5e3f565 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 21 Jul 2021 11:42:38 +0300 Subject: [PATCH 001/403] spring.jpa.properties.hibernate.order_by.default_null_ordering: "none" (remove NULLS LAST from queries) --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0cede00913..93023a3d19 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -463,7 +463,7 @@ spring.servlet.multipart.max-file-size: "50MB" spring.servlet.multipart.max-request-size: "50MB" spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" -spring.jpa.properties.hibernate.order_by.default_null_ordering: "last" +spring.jpa.properties.hibernate.order_by.default_null_ordering: "none" # SQL DAO Configuration spring: From a01ac25a5ac1fc56298c620976fd5e5087057671 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Fri, 30 Jul 2021 18:00:05 +0300 Subject: [PATCH 002/403] added parameter SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING to maintain possibility to override this settings. Those may be useful for existing Thingsboard instance that strictly tuned on previous setting for DESC NULLS LAST --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 93023a3d19..33889f92f3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -463,7 +463,7 @@ spring.servlet.multipart.max-file-size: "50MB" spring.servlet.multipart.max-request-size: "50MB" spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation: "true" -spring.jpa.properties.hibernate.order_by.default_null_ordering: "none" +spring.jpa.properties.hibernate.order_by.default_null_ordering: "${SPRING_JPA_PROPERTIES_HIBERNATE_ORDER_BY_DEFAULT_NULL_ORDERING:none}" # SQL DAO Configuration spring: From 55394f6367a877c118156d7d53864fb1e32d4552 Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 17 Sep 2021 13:11:51 +0300 Subject: [PATCH 003/403] add REST API for fetching TB application version --- .../controller/SystemInfoController.java | 67 +++++++++++++++++++ pom.xml | 1 + 2 files changed, 68 insertions(+) create mode 100644 application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java new file mode 100644 index 0000000000..1fd3819880 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.BuildProperties; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import javax.annotation.PostConstruct; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@Slf4j +public class SystemInfoController { + + @Autowired + private BuildProperties buildProperties; + + @PostConstruct + public void init() { + JsonNode info = buildInfoObject(); + log.info("System build info: {}", info); + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/system/info", method = RequestMethod.GET) + @ResponseBody + public JsonNode getSystemVersionInfo() { + return buildInfoObject(); + } + + private JsonNode buildInfoObject() { + ObjectMapper objectMapper = new ObjectMapper(); + ObjectNode infoObject = objectMapper.createObjectNode(); + if (buildProperties != null) { + infoObject.put("version", buildProperties.getVersion()); + infoObject.put("artifact", buildProperties.getArtifact()); + infoObject.put("name", buildProperties.getName()); + } else { + infoObject.put("version", "unknown"); + } + return infoObject; + } +} diff --git a/pom.xml b/pom.xml index fa33548a56..b638ea55ff 100755 --- a/pom.xml +++ b/pom.xml @@ -497,6 +497,7 @@ repackage + build-info From 19d08428ffd8ca07c98881295b8f0fd4d88301bc Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 17 Sep 2021 13:12:50 +0300 Subject: [PATCH 004/403] add support for multi-root relation queries --- .../data/query/RelationsQueryFilter.java | 3 + .../server/dao/entity/BaseEntityService.java | 12 ++ .../query/DefaultEntityQueryRepository.java | 30 ++- .../dao/sql/query/EntityKeyMapping.java | 4 +- .../dao/service/BaseEntityServiceTest.java | 180 ++++++++++++++++++ 5 files changed, 221 insertions(+), 8 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 9c113eb793..4409ebdab6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; +import java.util.Set; @Data public class RelationsQueryFilter implements EntityFilter { @@ -31,6 +32,8 @@ public class RelationsQueryFilter implements EntityFilter { } private EntityId rootEntity; + private boolean isMultiRoot; + private Set multiRootEntities; private EntitySearchDirection direction; private List filters; private int maxLevel; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 7686d9ec21..6c3fe1fd24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -42,6 +42,8 @@ import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityFilterType; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.customer.CustomerService; @@ -228,6 +230,8 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe throw new IncorrectParameterException("Query entity filter must be specified."); } else if (query.getEntityFilter().getType() == null) { throw new IncorrectParameterException("Query entity filter type must be specified."); + } else if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { + validateRelationQuery((RelationsQueryFilter) query.getEntityFilter()); } } @@ -246,4 +250,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } } + private static void validateRelationQuery(RelationsQueryFilter queryFilter) { + if(queryFilter.isMultiRoot() && queryFilter.getMultiRootEntities().isEmpty()) { + throw new IncorrectParameterException("Multi-root relation query filter should contain root entities"); + } + if(!queryFilter.isMultiRoot() && queryFilter.getRootEntity() == null) { + throw new IncorrectParameterException("Relation query filter root entity should not be blank"); + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 7b3ea36cfa..6a4212df6f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -220,6 +220,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { " THEN (select additional_info from edge where id = entity_id)" + " END as additional_info"; + private static final String SELECT_RELATED_PARENT_ID = "entity.parentId AS parentId"; + private static final String SELECT_API_USAGE_STATE = "(select aus.id, aus.created_time, aus.tenant_id, aus.entity_id, " + "coalesce((select title from tenant where id = aus.entity_id), (select title from customer where id = aus.entity_id)) as name " + "from api_usage_state as aus)"; @@ -242,16 +244,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, relation_type, lvl) AS (" + " SELECT from_id, from_type, to_id, to_type, relation_type, 1 as lvl" + " FROM relation" + - " WHERE $in_id = :relation_root_id and $in_type = :relation_root_type and relation_type_group = 'COMMON'" + + " WHERE $in_id $rootIdCondition and $in_type = :relation_root_type and relation_type_group = 'COMMON'" + " UNION ALL" + " SELECT r.from_id, r.from_type, r.to_id, r.to_type, r.relation_type, lvl + 1" + " FROM relation r" + " INNER JOIN related_entities re ON" + " r.$in_id = re.$out_id and r.$in_type = re.$out_type and" + " relation_type_group = 'COMMON' %s)" + - " SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" + + " SELECT re.$out_id entity_id, re.$out_type entity_type, re.$in_id parentId, max(re.lvl) lvl" + " from related_entities re" + - " %s GROUP BY entity_id, entity_type) entity"; + " %s GROUP BY entity_id, entity_type, parentId) entity"; private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from"); private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to"); @@ -562,6 +564,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { + (entityType.equals(EntityType.ENTITY_VIEW) ? "" : ", label ") + "FROM " + entityType.name() + " WHERE id in ( SELECT entity_id"; String from = getQueryTemplate(entityFilter.getDirection()); + from = from.replace("$rootIdCondition", "= :relation_root_id"); String whereFilter = " WHERE"; if (!StringUtils.isEmpty(entityFilter.getRelationType())) { ctx.addStringParameter("where_relation_type", entityFilter.getRelationType()); @@ -604,11 +607,20 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { + SELECT_TYPE + ", " + SELECT_NAME + ", " + SELECT_LABEL + ", " + SELECT_FIRST_NAME + ", " + SELECT_LAST_NAME + ", " + SELECT_EMAIL + ", " + SELECT_REGION + ", " + SELECT_TITLE + ", " + SELECT_COUNTRY + ", " + SELECT_STATE + ", " + SELECT_CITY + ", " + - SELECT_ADDRESS + ", " + SELECT_ADDRESS_2 + ", " + SELECT_ZIP + ", " + SELECT_PHONE + ", " + SELECT_ADDITIONAL_INFO + + SELECT_ADDRESS + ", " + SELECT_ADDRESS_2 + ", " + SELECT_ZIP + ", " + SELECT_PHONE + ", " + + SELECT_ADDITIONAL_INFO + ", " + SELECT_RELATED_PARENT_ID + ", entity.entity_type as entity_type"; String from = getQueryTemplate(entityFilter.getDirection()); - ctx.addUuidParameter("relation_root_id", rootId.getId()); - ctx.addStringParameter("relation_root_type", rootId.getEntityType().name()); + + if(entityFilter.isMultiRoot()) { + ctx.addUuidListParameter("relation_root_ids", entityFilter.getMultiRootEntities().stream().map(EntityId::getId).collect(Collectors.toList())); + ctx.addStringParameter("relation_root_type", entityFilter.getMultiRootEntities().stream().map(EntityId::getEntityType).findFirst().get().name()); + from = from.replace("$rootIdCondition", "in (:relation_root_ids)"); + } else { + ctx.addUuidParameter("relation_root_id", rootId.getId()); + ctx.addStringParameter("relation_root_type", rootId.getEntityType().name()); + from = from.replace("$rootIdCondition", "= :relation_root_id"); + } StringBuilder whereFilter = new StringBuilder(); @@ -790,7 +802,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case EDGE_SEARCH_QUERY: return EntityType.EDGE; case RELATIONS_QUERY: - return ((RelationsQueryFilter) entityFilter).getRootEntity().getEntityType(); + RelationsQueryFilter rgf = (RelationsQueryFilter) entityFilter; + if(rgf.isMultiRoot()) { + return rgf.getMultiRootEntities().iterator().next().getEntityType(); + } + return rgf.getRootEntity().getEntityType(); case API_USAGE_STATE: return EntityType.API_USAGE_STATE; default: diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index caed48758a..3aab5ee63e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -72,6 +72,7 @@ public class EntityKeyMapping { public static final String ZIP = "zip"; public static final String PHONE = "phone"; public static final String ADDITIONAL_INFO = "additionalInfo"; + public static final String RELATED_PARENT_ID = "parentId"; public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); public static final List widgetEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME); @@ -82,7 +83,7 @@ public class EntityKeyMapping { public static final Set apiUsageStateEntityFields = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME)); public static final Set commonEntityFieldsSet = new HashSet<>(commonEntityFields); - public static final Set relationQueryEntityFieldsSet = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, FIRST_NAME, LAST_NAME, EMAIL, REGION, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO)); + public static final Set relationQueryEntityFieldsSet = new HashSet<>(Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, LABEL, FIRST_NAME, LAST_NAME, EMAIL, REGION, TITLE, COUNTRY, STATE, CITY, ADDRESS, ADDRESS_2, ZIP, PHONE, ADDITIONAL_INFO, RELATED_PARENT_ID)); static { allowedEntityFieldMap.put(EntityType.DEVICE, new HashSet<>(labeledEntityFields)); @@ -120,6 +121,7 @@ public class EntityKeyMapping { entityFieldColumnMap.put(ZIP, ModelConstants.ZIP_PROPERTY); entityFieldColumnMap.put(PHONE, ModelConstants.PHONE_PROPERTY); entityFieldColumnMap.put(ADDITIONAL_INFO, ModelConstants.ADDITIONAL_INFO_PROPERTY); + entityFieldColumnMap.put(RELATED_PARENT_ID, RELATED_PARENT_ID); Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 831d92965b..ab8d8233e1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.dao.service; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -75,8 +78,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -373,6 +379,108 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } + @Test + public void testCountHierarchicalEntitiesByMultiRootQuery() throws InterruptedException { + List buildings = new ArrayList<>(); + List apartments = new ArrayList<>(); + Map> entityNameByTypeMap = new HashMap<>(); + Map childParentRelationMap = new HashMap<>(); + createMultiRootHierarchy(buildings, apartments, entityNameByTypeMap, childParentRelationMap); + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setMultiRoot(true); + filter.setMultiRootEntities(buildings.stream().map(IdBased::getId).collect(Collectors.toSet())); + filter.setDirection(EntitySearchDirection.FROM); + + EntityCountQuery countQuery = new EntityCountQuery(filter); + + long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(63, count); + + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("AptToHeat", Collections.singletonList(EntityType.DEVICE)))); + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(27, count); + + filter.setMultiRootEntities(apartments.stream().map(IdBased::getId).collect(Collectors.toSet())); + filter.setDirection(EntitySearchDirection.TO); + filter.setFilters(Lists.newArrayList( + new RelationEntityTypeFilter("buildingToApt", Collections.singletonList(EntityType.ASSET)), + new RelationEntityTypeFilter("AptToEnergy", Collections.singletonList(EntityType.DEVICE)))); + + count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); + Assert.assertEquals(9, count); + + deviceService.deleteDevicesByTenantId(tenantId); + assetService.deleteAssetsByTenantId(tenantId); + + } + + @Test + public void testMultiRootHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException { + List buildings = new ArrayList<>(); + List apartments = new ArrayList<>(); + Map> entityNameByTypeMap = new HashMap<>(); + Map childParentRelationMap = new HashMap<>(); + createMultiRootHierarchy(buildings, apartments, entityNameByTypeMap, childParentRelationMap); + + RelationsQueryFilter filter = new RelationsQueryFilter(); + filter.setMultiRoot(true); + filter.setMultiRootEntities(buildings.stream().map(IdBased::getId).collect(Collectors.toSet())); + filter.setDirection(EntitySearchDirection.FROM); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = Lists.newArrayList( + new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "parentId"), + new EntityKey(EntityKeyType.ENTITY_FIELD, "type") + ); + List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "status")); + + KeyFilter onlineStatusFilter = new KeyFilter(); + onlineStatusFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(StringOperation.ENDS_WITH); + predicate.setValue(FilterPredicateValue.fromString("_1")); + onlineStatusFilter.setPredicate(predicate); + List keyFilters = Collections.singletonList(onlineStatusFilter); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + List loadedEntities = new ArrayList<>(data.getData()); + while (data.hasNext()) { + query = query.next(); + data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + loadedEntities.addAll(data.getData()); + } + + long expectedEntitiesCnt = entityNameByTypeMap.entrySet() + .stream() + .filter(e -> !e.getKey().equals("building")) + .flatMap(e -> e.getValue().entrySet().stream()) + .map(Map.Entry::getValue) + .filter(e -> StringUtils.endsWith(e, "_1")) + .count(); + Assert.assertEquals(expectedEntitiesCnt, loadedEntities.size()); + + Map actualRelations = new HashMap<>(); + loadedEntities.forEach(ed -> { + UUID parentId = UUID.fromString(ed.getLatest().get(EntityKeyType.ENTITY_FIELD).get("parentId").getValue()); + UUID entityId = ed.getEntityId().getId(); + Assert.assertEquals(childParentRelationMap.get(entityId), parentId); + actualRelations.put(entityId, parentId); + + String entityType = ed.getLatest().get(EntityKeyType.ENTITY_FIELD).get("type").getValue(); + String actualEntityName = ed.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue(); + String expectedEntityName = entityNameByTypeMap.get(entityType).get(entityId); + Assert.assertEquals(expectedEntityName, actualEntityName); + }); + + deviceService.deleteDevicesByTenantId(tenantId); + assetService.deleteAssetsByTenantId(tenantId); + } @Test public void testHierarchicalFindDevicesWithAttributesByQuery() throws ExecutionException, InterruptedException { @@ -1534,4 +1642,76 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); return timeseriesService.save(SYSTEM_TENANT_ID, entityId, timeseries); } + + private void createMultiRootHierarchy(List buildings, List apartments, + Map> entityNameByTypeMap, + Map childParentRelationMap) throws InterruptedException { + for (int k = 0; k < 3; k++) { + Asset building = new Asset(); + building.setTenantId(tenantId); + building.setName("Building _" + k); + building.setType("building"); + building.setLabel("building label" + k); + building = assetService.saveAsset(building); + buildings.add(building); + entityNameByTypeMap.computeIfAbsent(building.getType(), n -> new HashMap<>()).put(building.getId().getId(), building.getName()); + + for (int i = 0; i < 3; i++) { + Asset asset = new Asset(); + asset.setTenantId(tenantId); + asset.setName("Apt " + k + "_" + i); + asset.setType("apartment"); + asset.setLabel("apartment " + i); + asset = assetService.saveAsset(asset); + //TO make sure devices have different created time + Thread.sleep(1); + entityNameByTypeMap.computeIfAbsent(asset.getType(), n -> new HashMap<>()).put(asset.getId().getId(), asset.getName()); + apartments.add(asset); + EntityRelation er = new EntityRelation(); + er.setFrom(building.getId()); + er.setTo(asset.getId()); + er.setType("buildingToApt"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + childParentRelationMap.put(asset.getUuidId(), building.getUuidId()); + for (int j = 0; j < 3; j++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Heat" + k + "_" + i + "_" + j); + device.setType("heatmeter"); + device.setLabel("heatmeter" + (int) (Math.random() * 1000)); + device = deviceService.saveDevice(device); + //TO make sure devices have different created time + Thread.sleep(1); + entityNameByTypeMap.computeIfAbsent(device.getType(), n -> new HashMap<>()).put(device.getId().getId(), device.getName()); + er = new EntityRelation(); + er.setFrom(asset.getId()); + er.setTo(device.getId()); + er.setType("AptToHeat"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + childParentRelationMap.put(device.getUuidId(), asset.getUuidId()); + } + + for (int j = 0; j < 3; j++) { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("Energy" + k + "_" + i + "_" + j); + device.setType("energymeter"); + device.setLabel("energymeter" + (int) (Math.random() * 1000)); + device = deviceService.saveDevice(device); + //TO make sure devices have different created time + Thread.sleep(1); + entityNameByTypeMap.computeIfAbsent(device.getType(), n -> new HashMap<>()).put(device.getId().getId(), device.getName()); + er = new EntityRelation(); + er.setFrom(asset.getId()); + er.setTo(device.getId()); + er.setType("AptToEnergy"); + er.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(tenantId, er); + childParentRelationMap.put(device.getUuidId(), asset.getUuidId()); + } + } + } + } } From 5a7673a118787834f1ec06d5b742af3cc777383b Mon Sep 17 00:00:00 2001 From: hanyuepeng Date: Wed, 10 Nov 2021 17:16:17 +0800 Subject: [PATCH 005/403] fix(chinese translate): fix some display problem In Api Usage module,when use chinese ,there are some display problem because of the lose of the translate items --- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 270fd6ad4e..39f4063b49 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -388,7 +388,14 @@ "transport-messages": "传输消息", "transport-monthly-activity": "Transport monthly activity", "view-details": "查看详细信息", - "view-statistics": "查看统计信息" + "view-statistics": "查看统计信息", + "alarm":"警告", + "alarms-created": "创建警告数", + "alarms-created-hourly-activity":"Alarms created hourly activity", + "alarms-created-daily-activity": "Alarms created daily activity", + "alarms-created-monthly-activity": "Alarms created monthly activity", + "notifications": "通知", + "notifications-hourly-activity": "Notifications hourly activity" }, "asset": { "add": "添加资产", From 524ff2f980cf4a5baa963dc29aaf2455e1e7c4d7 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Wed, 10 Nov 2021 14:36:28 +0200 Subject: [PATCH 006/403] Update locale.constant-zh_CN.json --- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 39f4063b49..f8e7e810fe 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -391,11 +391,7 @@ "view-statistics": "查看统计信息", "alarm":"警告", "alarms-created": "创建警告数", - "alarms-created-hourly-activity":"Alarms created hourly activity", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", - "notifications": "通知", - "notifications-hourly-activity": "Notifications hourly activity" + "notifications": "通知" }, "asset": { "add": "添加资产", From 79f3d3826a1c916288c8a89b15c9be64d068d9ef Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 12 Nov 2021 15:52:58 +0200 Subject: [PATCH 007/403] UI: Added clearing selectedWidgetsBundleAlias when calling resetSortAndFilter function in attribute table --- .../home/components/attribute/attribute-table.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index bcb9edd07e..9e9bbceb6f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -263,6 +263,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.attributeScopeSelectionReadonly = true; } this.mode = 'default'; + this.selectedWidgetsBundleAlias = null; this.attributeScope = this.defaultAttributeScope; this.pageLink.textSearch = null; if (this.viewsInited) { From 5d4579ce288bcd6750b9a8a82e80f94d757c26ed Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 26 Nov 2021 17:00:51 +0200 Subject: [PATCH 008/403] UI: New control widget Persistent table --- .../widget_bundles/control_widgets.json | 18 + .../server/controller/RpcV2Controller.java | 11 +- .../server/dao/rpc/RpcService.java | 2 + .../server/dao/rpc/BaseRpcService.java | 10 +- .../thingsboard/server/dao/rpc/RpcDao.java | 4 +- .../server/dao/sql/rpc/JpaRpcDao.java | 7 +- .../server/dao/sql/rpc/RpcRepository.java | 2 + ui-ngx/src/app/core/api/widget-api.models.ts | 14 +- .../src/app/core/api/widget-subscription.ts | 23 +- ui-ngx/src/app/core/http/device.service.ts | 13 +- .../rpc/persistent-add-dialog.component.html | 92 ++++ .../rpc/persistent-add-dialog.component.scss | 30 ++ .../rpc/persistent-add-dialog.component.ts | 77 +++ .../persistent-details-dialog.component.html | 118 +++++ .../persistent-details-dialog.component.scss | 33 ++ .../persistent-details-dialog.component.ts | 151 ++++++ .../persistent-filter-panel.component.html | 44 ++ .../persistent-filter-panel.component.scss | 36 ++ .../rpc/persistent-filter-panel.component.ts | 71 +++ .../lib/rpc/persistent-table.component.html | 125 +++++ .../lib/rpc/persistent-table.component.scss | 52 ++ .../lib/rpc/persistent-table.component.ts | 476 ++++++++++++++++++ .../widget/lib/rpc/rpc-widgets.module.ts | 15 +- .../home/models/widget-component.models.ts | 10 +- .../json-object-view.component.html | 22 + .../json-object-view.component.scss | 27 + .../components/json-object-view.component.ts | 166 ++++++ ui-ngx/src/app/shared/models/rpc.models.ts | 51 +- ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 44 +- .../assets/locale/locale.constant-ru_RU.json | 41 ++ .../assets/locale/locale.constant-uk_UA.json | 41 ++ 32 files changed, 1805 insertions(+), 24 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.html create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.scss create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index f1e3eaa1fe..73a1ef633d 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -149,6 +149,24 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" } + }, + { + "alias": "persistent_table", + "name": "Persistent table", + "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yMDAgMEgwVjE2MEgyMDBWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMDAgMTIwSDBWMTIxSDIwMFYxMjBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgODBIMFY4MUgyMDBWODBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgMzlIMFY0MEgyMDBWMzlaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0xNS42Njg1IDE5Ljk4NjhIMTQuMTUzOFYyM0gxMi43OTQ5VjE1LjE3OTdIMTUuNTQ0OUMxNi40NDczIDE1LjE3OTcgMTcuMTQzNyAxNS4zODIgMTcuNjM0MyAxNS43ODY2QzE4LjEyNDggMTYuMTkxMiAxOC4zNzAxIDE2Ljc3NjcgMTguMzcwMSAxNy41NDNDMTguMzcwMSAxOC4wNjU4IDE4LjI0MyAxOC41MDQ0IDE3Ljk4ODggMTguODU4OUMxNy43MzgxIDE5LjIwOTggMTcuMzg3MiAxOS40ODAxIDE2LjkzNiAxOS42Njk5TDE4LjY5MjQgMjIuOTMwMlYyM0gxNy4yMzY4TDE1LjY2ODUgMTkuOTg2OFpNMTQuMTUzOCAxOC44OTY1SDE1LjU1MDNDMTYuMDA4NiAxOC44OTY1IDE2LjM2NjcgMTguNzgxOSAxNi42MjQ1IDE4LjU1MjdDMTYuODgyMyAxOC4zMiAxNy4wMTEyIDE4LjAwMzEgMTcuMDExMiAxNy42MDIxQzE3LjAxMTIgMTcuMTgzMSAxNi44OTEzIDE2Ljg1OSAxNi42NTE0IDE2LjYyOTlDMTYuNDE1IDE2LjQwMDcgMTYuMDYwNSAxNi4yODI2IDE1LjU4NzkgMTYuMjc1NEgxNC4xNTM4VjE4Ljg5NjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yMS4wMTgxIDIwLjA5NDJWMjNIMTkuNjU5MlYxNS4xNzk3SDIyLjY1MDlDMjMuNTI0NiAxNS4xNzk3IDI0LjIxNzQgMTUuNDA3MSAyNC43Mjk1IDE1Ljg2MThDMjUuMjQ1MSAxNi4zMTY2IDI1LjUwMjkgMTYuOTE4MSAyNS41MDI5IDE3LjY2NjVDMjUuNTAyOSAxOC40MzI4IDI1LjI1MDUgMTkuMDI5IDI0Ljc0NTYgMTkuNDU1MUMyNC4yNDQzIDE5Ljg4MTIgMjMuNTQwNyAyMC4wOTQyIDIyLjYzNDggMjAuMDk0MkgyMS4wMTgxWk0yMS4wMTgxIDE5LjAwMzlIMjIuNjUwOUMyMy4xMzQzIDE5LjAwMzkgMjMuNTAzMSAxOC44OTExIDIzLjc1NzMgMTguNjY1NUMyNC4wMTE2IDE4LjQzNjQgMjQuMTM4NyAxOC4xMDY5IDI0LjEzODcgMTcuNjc3MkMyNC4xMzg3IDE3LjI1NDcgMjQuMDA5OCAxNi45MTgxIDIzLjc1MiAxNi42Njc1QzIzLjQ5NDEgMTYuNDEzMiAyMy4xMzk2IDE2LjI4MjYgMjIuNjg4NSAxNi4yNzU0SDIxLjAxODFWMTkuMDAzOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMyLjY2MjYgMjAuNDU0MUMzMi41ODM4IDIxLjI4ODQgMzIuMjc1OSAyMS45NDAxIDMxLjczODggMjIuNDA5MkMzMS4yMDE3IDIyLjg3NDcgMzAuNDg3MyAyMy4xMDc0IDI5LjU5NTcgMjMuMTA3NEMyOC45NzI3IDIzLjEwNzQgMjguNDIzIDIyLjk2MDYgMjcuOTQ2OCAyMi42NjdDMjcuNDc0MSAyMi4zNjk4IDI3LjEwODkgMjEuOTQ5MSAyNi44NTExIDIxLjQwNDhDMjYuNTkzMyAyMC44NjA1IDI2LjQ1OSAyMC4yMjg1IDI2LjQ0ODIgMTkuNTA4OFYxOC43NzgzQzI2LjQ0ODIgMTguMDQwNyAyNi41Nzg5IDE3LjM5MDggMjYuODQwMyAxNi44Mjg2QzI3LjEwMTcgMTYuMjY2NCAyNy40NzU5IDE1LjgzMzIgMjcuOTYyOSAxNS41Mjg4QzI4LjQ1MzUgMTUuMjI0NCAyOS4wMTkyIDE1LjA3MjMgMjkuNjYwMiAxNS4wNzIzQzMwLjUyMzEgMTUuMDcyMyAzMS4yMTc4IDE1LjMwNjggMzEuNzQ0MSAxNS43NzU5QzMyLjI3MDUgMTYuMjQ1IDMyLjU3NjcgMTYuOTA3NCAzMi42NjI2IDE3Ljc2MzJIMzEuMzA5MUMzMS4yNDQ2IDE3LjIwMSAzMS4wNzk5IDE2Ljc5NjQgMzAuODE0OSAxNi41NDkzQzMwLjU1MzUgMTYuMjk4NyAzMC4xNjg2IDE2LjE3MzMgMjkuNjYwMiAxNi4xNzMzQzI5LjA2OTMgMTYuMTczMyAyOC42MTQ2IDE2LjM5IDI4LjI5NTkgMTYuODIzMkMyNy45ODA4IDE3LjI1MjkgMjcuODE5NyAxNy44ODQ5IDI3LjgxMjUgMTguNzE5MlYxOS40MTIxQzI3LjgxMjUgMjAuMjU3MiAyNy45NjI5IDIwLjkwMTcgMjguMjYzNyAyMS4zNDU3QzI4LjU2OCAyMS43ODk3IDI5LjAxMiAyMi4wMTE3IDI5LjU5NTcgMjIuMDExN0MzMC4xMjkyIDIyLjAxMTcgMzAuNTMwMyAyMS44OTE4IDMwLjc5ODggMjEuNjUxOUMzMS4wNjc0IDIxLjQxMTkgMzEuMjM3NSAyMS4wMTI3IDMxLjMwOTEgMjAuNDU0MUgzMi42NjI2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMzguMDU1MiAyM0gzNi43MDE3VjE1LjE3OTdIMzguMDU1MlYyM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTM5LjcyNTYgMjNWMTUuMTc5N0g0Mi4wMzUyQzQyLjcyNjIgMTUuMTc5NyA0My4zMzg1IDE1LjMzMzcgNDMuODcyMSAxNS42NDE2QzQ0LjQwOTIgMTUuOTQ5NSA0NC44MjQ1IDE2LjM4NjQgNDUuMTE4MiAxNi45NTIxQzQ1LjQxMTggMTcuNTE3OSA0NS41NTg2IDE4LjE2NiA0NS41NTg2IDE4Ljg5NjVWMTkuMjg4NkM0NS41NTg2IDIwLjAyOTggNDUuNDEgMjAuNjgxNSA0NS4xMTI4IDIxLjI0MzdDNDQuODE5MiAyMS44MDU4IDQ0LjM5ODQgMjIuMjM5MSA0My44NTA2IDIyLjU0MzVDNDMuMzA2MyAyMi44NDc4IDQyLjY4MTUgMjMgNDEuOTc2MSAyM0gzOS43MjU2Wk00MS4wODQ1IDE2LjI3NTRWMjEuOTE1SDQxLjk3MDdDNDIuNjgzMyAyMS45MTUgNDMuMjI5MyAyMS42OTMgNDMuNjA4OSAyMS4yNDlDNDMuOTkyIDIwLjgwMTQgNDQuMTg3MiAyMC4xNjA1IDQ0LjE5NDMgMTkuMzI2MlYxOC44OTExQzQ0LjE5NDMgMTguMDQyNSA0NC4wMDk5IDE3LjM5NDQgNDMuNjQxMSAxNi45NDY4QzQzLjI3MjMgMTYuNDk5MiA0Mi43MzcgMTYuMjc1NCA0Mi4wMzUyIDE2LjI3NTRINDEuMDg0NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTYxLjU1NjYgMTUuMTc5N0w2My44MTI1IDIxLjE3MzhMNjYuMDYzIDE1LjE3OTdINjcuODE5M1YyM0g2Ni40NjU4VjIwLjQyMTlMNjYuNjAwMSAxNi45NzM2TDY0LjI5MDUgMjNINjMuMzE4NEw2MS4wMTQyIDE2Ljk3OUw2MS4xNDg0IDIwLjQyMTlWMjNINTkuNzk0OVYxNS4xNzk3SDYxLjU1NjZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03MS44NjM4IDIzLjEwNzRDNzEuMDM2NiAyMy4xMDc0IDcwLjM2NTIgMjIuODQ3OCA2OS44NDk2IDIyLjMyODZDNjkuMzM3NiAyMS44MDU4IDY5LjA4MTUgMjEuMTExMiA2OS4wODE1IDIwLjI0NDZWMjAuMDgzNUM2OS4wODE1IDE5LjUwMzQgNjkuMTkyNSAxOC45ODYgNjkuNDE0NiAxOC41MzEyQzY5LjY0MDEgMTguMDcyOSA2OS45NTUyIDE3LjcxNjYgNzAuMzU5OSAxNy40NjI0QzcwLjc2NDUgMTcuMjA4MiA3MS4yMTU3IDE3LjA4MTEgNzEuNzEzNCAxNy4wODExQzcyLjUwNDcgMTcuMDgxMSA3My4xMTUyIDE3LjMzMzUgNzMuNTQ0OSAxNy44Mzg0QzczLjk3ODIgMTguMzQzMyA3NC4xOTQ4IDE5LjA1NzYgNzQuMTk0OCAxOS45ODE0VjIwLjUwNzhINzAuMzk3NUM3MC40MzY4IDIwLjk4NzYgNzAuNTk2MiAyMS4zNjcyIDcwLjg3NTUgMjEuNjQ2NUM3MS4xNTg0IDIxLjkyNTggNzEuNTEyOSAyMi4wNjU0IDcxLjkzOSAyMi4wNjU0QzcyLjUzNjkgMjIuMDY1NCA3My4wMjM5IDIxLjgyMzcgNzMuMzk5OSAyMS4zNDAzTDc0LjEwMzUgMjIuMDExN0M3My44NzA4IDIyLjM1OSA3My41NTkyIDIyLjYyOTQgNzMuMTY4OSAyMi44MjI4QzcyLjc4MjIgMjMuMDEyNSA3Mi4zNDcyIDIzLjEwNzQgNzEuODYzOCAyMy4xMDc0Wk03MS43MDggMTguMTI4NEM3MS4zNDk5IDE4LjEyODQgNzEuMDU5OSAxOC4yNTM3IDcwLjgzNzkgMTguNTA0NEM3MC42MTk1IDE4Ljc1NSA3MC40Nzk4IDE5LjEwNDIgNzAuNDE4OSAxOS41NTE4SDcyLjkwNThWMTkuNDU1MUM3Mi44NzcxIDE5LjAxODIgNzIuNzYwNyAxOC42ODg4IDcyLjU1NjYgMTguNDY2OEM3Mi4zNTI1IDE4LjI0MTIgNzIuMDY5NyAxOC4xMjg0IDcxLjcwOCAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNzguNDcwMiAyMS40MjA5Qzc4LjQ3MDIgMjEuMTg4MiA3OC4zNzM1IDIxLjAxMDkgNzguMTgwMiAyMC44ODkyQzc3Ljk5MDQgMjAuNzY3NCA3Ny42NzM1IDIwLjY2IDc3LjIyOTUgMjAuNTY2OUM3Ni43ODU1IDIwLjQ3MzggNzYuNDE0OSAyMC4zNTU2IDc2LjExNzcgMjAuMjEyNEM3NS40NjYgMTkuODk3MyA3NS4xNDAxIDE5LjQ0MDggNzUuMTQwMSAxOC44NDI4Qzc1LjE0MDEgMTguMzQxNSA3NS4zNTE0IDE3LjkyMjUgNzUuNzczOSAxNy41ODU5Qzc2LjE5NjUgMTcuMjQ5MyA3Ni43MzM2IDE3LjA4MTEgNzcuMzg1MyAxNy4wODExQzc4LjA3OTkgMTcuMDgxMSA3OC42NDAzIDE3LjI1MjkgNzkuMDY2NCAxNy41OTY3Qzc5LjQ5NjEgMTcuOTQwNCA3OS43MTA5IDE4LjM4NjIgNzkuNzEwOSAxOC45MzQxSDc4LjQwNThDNzguNDA1OCAxOC42ODM0IDc4LjMxMjcgMTguNDc1NyA3OC4xMjY1IDE4LjMxMUM3Ny45NDAzIDE4LjE0MjcgNzcuNjkzMiAxOC4wNTg2IDc3LjM4NTMgMTguMDU4NkM3Ny4wOTg4IDE4LjA1ODYgNzYuODY0MyAxOC4xMjQ4IDc2LjY4MTYgMTguMjU3M0M3Ni41MDI2IDE4LjM4OTggNzYuNDEzMSAxOC41NjcxIDc2LjQxMzEgMTguNzg5MUM3Ni40MTMxIDE4Ljk4OTYgNzYuNDk3MiAxOS4xNDUzIDc2LjY2NTUgMTkuMjU2M0M3Ni44MzM4IDE5LjM2NzQgNzcuMTc0IDE5LjQ4MDEgNzcuNjg2IDE5LjU5NDdDNzguMTk4MSAxOS43MDU3IDc4LjU5OTEgMTkuODQgNzguODg5MiAxOS45OTc2Qzc5LjE4MjggMjAuMTUxNSA3OS4zOTk0IDIwLjMzNzcgNzkuNTM5MSAyMC41NTYyQzc5LjY4MjMgMjAuNzc0NiA3OS43NTM5IDIxLjAzOTYgNzkuNzUzOSAyMS4zNTExQzc5Ljc1MzkgMjEuODczOSA3OS41MzczIDIyLjI5ODIgNzkuMTA0IDIyLjYyNEM3OC42NzA3IDIyLjk0NjMgNzguMTAzMiAyMy4xMDc0IDc3LjQwMTQgMjMuMTA3NEM3Ni45MjUxIDIzLjEwNzQgNzYuNTAwOCAyMy4wMjE1IDc2LjEyODQgMjIuODQ5NkM3NS43NTYgMjIuNjc3NyA3NS40NjYgMjIuNDQxNCA3NS4yNTgzIDIyLjE0MDZDNzUuMDUwNiAyMS44Mzk4IDc0Ljk0NjggMjEuNTE1OCA3NC45NDY4IDIxLjE2ODVINzYuMjE0NEM3Ni4yMzIzIDIxLjQ3NjQgNzYuMzQ4NiAyMS43MTQ1IDc2LjU2MzUgMjEuODgyOEM3Ni43NzgzIDIyLjA0NzUgNzcuMDYzIDIyLjEyOTkgNzcuNDE3NSAyMi4xMjk5Qzc3Ljc2MTIgMjIuMTI5OSA3OC4wMjI2IDIyLjA2NTQgNzguMjAxNyAyMS45MzY1Qzc4LjM4MDcgMjEuODA0IDc4LjQ3MDIgMjEuNjMyMiA3OC40NzAyIDIxLjQyMDlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04NC4xNTI4IDIxLjQyMDlDODQuMTUyOCAyMS4xODgyIDg0LjA1NjIgMjEuMDEwOSA4My44NjI4IDIwLjg4OTJDODMuNjczIDIwLjc2NzQgODMuMzU2MSAyMC42NiA4Mi45MTIxIDIwLjU2NjlDODIuNDY4MSAyMC40NzM4IDgyLjA5NzUgMjAuMzU1NiA4MS44MDAzIDIwLjIxMjRDODEuMTQ4NiAxOS44OTczIDgwLjgyMjggMTkuNDQwOCA4MC44MjI4IDE4Ljg0MjhDODAuODIyOCAxOC4zNDE1IDgxLjAzNCAxNy45MjI1IDgxLjQ1NjUgMTcuNTg1OUM4MS44NzkxIDE3LjI0OTMgODIuNDE2MiAxNy4wODExIDgzLjA2NzkgMTcuMDgxMUM4My43NjI1IDE3LjA4MTEgODQuMzIyOSAxNy4yNTI5IDg0Ljc0OSAxNy41OTY3Qzg1LjE3ODcgMTcuOTQwNCA4NS4zOTM2IDE4LjM4NjIgODUuMzkzNiAxOC45MzQxSDg0LjA4ODRDODQuMDg4NCAxOC42ODM0IDgzLjk5NTMgMTguNDc1NyA4My44MDkxIDE4LjMxMUM4My42MjI5IDE4LjE0MjcgODMuMzc1OCAxOC4wNTg2IDgzLjA2NzkgMTguMDU4NkM4Mi43ODE0IDE4LjA1ODYgODIuNTQ2OSAxOC4xMjQ4IDgyLjM2NDMgMTguMjU3M0M4Mi4xODUyIDE4LjM4OTggODIuMDk1NyAxOC41NjcxIDgyLjA5NTcgMTguNzg5MUM4Mi4wOTU3IDE4Ljk4OTYgODIuMTc5OSAxOS4xNDUzIDgyLjM0ODEgMTkuMjU2M0M4Mi41MTY0IDE5LjM2NzQgODIuODU2NiAxOS40ODAxIDgzLjM2ODcgMTkuNTk0N0M4My44ODA3IDE5LjcwNTcgODQuMjgxNyAxOS44NCA4NC41NzE4IDE5Ljk5NzZDODQuODY1NCAyMC4xNTE1IDg1LjA4MiAyMC4zMzc3IDg1LjIyMTcgMjAuNTU2MkM4NS4zNjQ5IDIwLjc3NDYgODUuNDM2NSAyMS4wMzk2IDg1LjQzNjUgMjEuMzUxMUM4NS40MzY1IDIxLjg3MzkgODUuMjE5OSAyMi4yOTgyIDg0Ljc4NjYgMjIuNjI0Qzg0LjM1MzQgMjIuOTQ2MyA4My43ODU4IDIzLjEwNzQgODMuMDg0IDIzLjEwNzRDODIuNjA3NyAyMy4xMDc0IDgyLjE4MzQgMjMuMDIxNSA4MS44MTEgMjIuODQ5NkM4MS40Mzg2IDIyLjY3NzcgODEuMTQ4NiAyMi40NDE0IDgwLjk0MDkgMjIuMTQwNkM4MC43MzMyIDIxLjgzOTggODAuNjI5NCAyMS41MTU4IDgwLjYyOTQgMjEuMTY4NUg4MS44OTdDODEuOTE0OSAyMS40NzY0IDgyLjAzMTIgMjEuNzE0NSA4Mi4yNDYxIDIxLjg4MjhDODIuNDYwOSAyMi4wNDc1IDgyLjc0NTYgMjIuMTI5OSA4My4xMDAxIDIyLjEyOTlDODMuNDQzOCAyMi4xMjk5IDgzLjcwNTIgMjIuMDY1NCA4My44ODQzIDIxLjkzNjVDODQuMDYzMyAyMS44MDQgODQuMTUyOCAyMS42MzIyIDg0LjE1MjggMjEuNDIwOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTkwLjA1MDMgMjNDODkuOTkzIDIyLjg4OSA4OS45NDI5IDIyLjcwODIgODkuODk5OSAyMi40NTc1Qzg5LjQ4NDUgMjIuODkwOCA4OC45NzYxIDIzLjEwNzQgODguMzc0NSAyMy4xMDc0Qzg3Ljc5MDkgMjMuMTA3NCA4Ny4zMTQ2IDIyLjk0MDkgODYuOTQ1OCAyMi42MDc5Qzg2LjU3NyAyMi4yNzQ5IDg2LjM5MjYgMjEuODYzMSA4Ni4zOTI2IDIxLjM3MjZDODYuMzkyNiAyMC43NTMxIDg2LjYyMTcgMjAuMjc4NiA4Ny4wODAxIDE5Ljk0OTJDODcuNTQyIDE5LjYxNjIgODguMjAwOCAxOS40NDk3IDg5LjA1NjYgMTkuNDQ5N0g4OS44NTY5VjE5LjA2ODRDODkuODU2OSAxOC43Njc2IDg5Ljc3MjggMTguNTI3NyA4OS42MDQ1IDE4LjM0ODZDODkuNDM2MiAxOC4xNjYgODkuMTgwMiAxOC4wNzQ3IDg4LjgzNjQgMTguMDc0N0M4OC41MzkyIDE4LjA3NDcgODguMjk1NyAxOC4xNDk5IDg4LjEwNiAxOC4zMDAzQzg3LjkxNjIgMTguNDQ3MSA4Ny44MjEzIDE4LjYzNTEgODcuODIxMyAxOC44NjQzSDg2LjUxNjFDODYuNTE2MSAxOC41NDU2IDg2LjYyMTcgMTguMjQ4NCA4Ni44MzMgMTcuOTcyN0M4Ny4wNDQzIDE3LjY5MzQgODcuMzMwNyAxNy40NzQ5IDg3LjY5MjQgMTcuMzE3NEM4OC4wNTc2IDE3LjE1OTggODguNDY0IDE3LjA4MTEgODguOTExNiAxNy4wODExQzg5LjU5MiAxNy4wODExIDkwLjEzNDQgMTcuMjUyOSA5MC41MzkxIDE3LjU5NjdDOTAuOTQzNyAxNy45MzY4IDkxLjE1MTQgMTguNDE2NyA5MS4xNjIxIDE5LjAzNjFWMjEuNjU3MkM5MS4xNjIxIDIyLjE4IDkxLjIzNTUgMjIuNTk3MiA5MS4zODIzIDIyLjkwODdWMjNIOTAuMDUwM1pNODguNjE2MiAyMi4wNjAxQzg4Ljg3NCAyMi4wNjAxIDg5LjExNTcgMjEuOTk3NCA4OS4zNDEzIDIxLjg3MjFDODkuNTcwNSAyMS43NDY3IDg5Ljc0MjQgMjEuNTc4NSA4OS44NTY5IDIxLjM2NzJWMjAuMjcxNUg4OS4xNTMzQzg4LjY2OTkgMjAuMjcxNSA4OC4zMDY1IDIwLjM1NTYgODguMDYzIDIwLjUyMzlDODcuODE5NSAyMC42OTIyIDg3LjY5NzggMjAuOTMwMyA4Ny42OTc4IDIxLjIzODNDODcuNjk3OCAyMS40ODg5IDg3Ljc4MDEgMjEuNjg5NSA4Ny45NDQ4IDIxLjgzOThDODguMTEzMSAyMS45ODY3IDg4LjMzNjkgMjIuMDYwMSA4OC42MTYyIDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05Mi4zMDA4IDIwLjA1MTNDOTIuMzAwOCAxOS4xNDg5IDkyLjUxMiAxOC40MjkyIDkyLjkzNDYgMTcuODkyMUM5My4zNjA3IDE3LjM1MTQgOTMuOTI0NiAxNy4wODExIDk0LjYyNjUgMTcuMDgxMUM5NS4yODg5IDE3LjA4MTEgOTUuODA5OSAxNy4zMTIgOTYuMTg5NSAxNy43NzM5TDk2LjI0ODUgMTcuMTg4NUg5Ny40MjQ4VjIyLjgyMjhDOTcuNDI0OCAyMy41ODU0IDk3LjE4NjcgMjQuMTg3IDk2LjcxMDQgMjQuNjI3NEM5Ni4yMzc4IDI1LjA2NzkgOTUuNTk4NiAyNS4yODgxIDk0Ljc5MyAyNS4yODgxQzk0LjM2NjkgMjUuMjg4MSA5My45NDk3IDI1LjE5ODYgOTMuNTQxNSAyNS4wMTk1QzkzLjEzNjkgMjQuODQ0MSA5Mi44Mjg5IDI0LjYxMzEgOTIuNjE3NyAyNC4zMjY3TDkzLjIzNTQgMjMuNTQyNUM5My42MzY0IDI0LjAxODcgOTQuMTMwNSAyNC4yNTY4IDk0LjcxNzggMjQuMjU2OEM5NS4xNTEgMjQuMjU2OCA5NS40OTMgMjQuMTM4NyA5NS43NDM3IDIzLjkwMjNDOTUuOTk0MyAyMy42Njk2IDk2LjExOTYgMjMuMzI1OCA5Ni4xMTk2IDIyLjg3MTFWMjIuNDc5Qzk1Ljc0MzcgMjIuODk3OSA5NS4yNDI0IDIzLjEwNzQgOTQuNjE1NyAyMy4xMDc0QzkzLjkzNTQgMjMuMTA3NCA5My4zNzg2IDIyLjgzNzEgOTIuOTQ1MyAyMi4yOTY0QzkyLjUxNTYgMjEuNzU1NyA5Mi4zMDA4IDIxLjAwNzMgOTIuMzAwOCAyMC4wNTEzWk05My42MDA2IDIwLjE2NDFDOTMuNjAwNiAyMC43NDc3IDkzLjcxODggMjEuMjA3OCA5My45NTUxIDIxLjU0NDRDOTQuMTk1IDIxLjg3NzQgOTQuNTI2MiAyMi4wNDM5IDk0Ljk0ODcgMjIuMDQzOUM5NS40NzUxIDIyLjA0MzkgOTUuODY1NCAyMS44MTg0IDk2LjExOTYgMjEuMzY3MlYxOC44MTA1Qzk1Ljg3MjYgMTguMzcwMSA5NS40ODU4IDE4LjE0OTkgOTQuOTU5NSAxOC4xNDk5Qzk0LjUyOTggMTguMTQ5OSA5NC4xOTUgMTguMzIgOTMuOTU1MSAxOC42NjAyQzkzLjcxODggMTkuMDAwMyA5My42MDA2IDE5LjUwMTYgOTMuNjAwNiAyMC4xNjQxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTAxLjMzIDIzLjEwNzRDMTAwLjUwMiAyMy4xMDc0IDk5LjgzMTEgMjIuODQ3OCA5OS4zMTU0IDIyLjMyODZDOTguODAzNCAyMS44MDU4IDk4LjU0NzQgMjEuMTExMiA5OC41NDc0IDIwLjI0NDZWMjAuMDgzNUM5OC41NDc0IDE5LjUwMzQgOTguNjU4NCAxOC45ODYgOTguODgwNCAxOC41MzEyQzk5LjEwNiAxOC4wNzI5IDk5LjQyMTEgMTcuNzE2NiA5OS44MjU3IDE3LjQ2MjRDMTAwLjIzIDE3LjIwODIgMTAwLjY4MSAxNy4wODExIDEwMS4xNzkgMTcuMDgxMUMxMDEuOTcxIDE3LjA4MTEgMTAyLjU4MSAxNy4zMzM1IDEwMy4wMTEgMTcuODM4NEMxMDMuNDQ0IDE4LjM0MzMgMTAzLjY2MSAxOS4wNTc2IDEwMy42NjEgMTkuOTgxNFYyMC41MDc4SDk5Ljg2MzNDOTkuOTAyNyAyMC45ODc2IDEwMC4wNjIgMjEuMzY3MiAxMDAuMzQxIDIxLjY0NjVDMTAwLjYyNCAyMS45MjU4IDEwMC45NzkgMjIuMDY1NCAxMDEuNDA1IDIyLjA2NTRDMTAyLjAwMyAyMi4wNjU0IDEwMi40OSAyMS44MjM3IDEwMi44NjYgMjEuMzQwM0wxMDMuNTY5IDIyLjAxMTdDMTAzLjMzNyAyMi4zNTkgMTAzLjAyNSAyMi42Mjk0IDEwMi42MzUgMjIuODIyOEMxMDIuMjQ4IDIzLjAxMjUgMTAxLjgxMyAyMy4xMDc0IDEwMS4zMyAyMy4xMDc0Wk0xMDEuMTc0IDE4LjEyODRDMTAwLjgxNiAxOC4xMjg0IDEwMC41MjYgMTguMjUzNyAxMDAuMzA0IDE4LjUwNDRDMTAwLjA4NSAxOC43NTUgOTkuOTQ1NiAxOS4xMDQyIDk5Ljg4NDggMTkuNTUxOEgxMDIuMzcyVjE5LjQ1NTFDMTAyLjM0MyAxOS4wMTgyIDEwMi4yMjcgMTguNjg4OCAxMDIuMDIyIDE4LjQ2NjhDMTAxLjgxOCAxOC4yNDEyIDEwMS41MzUgMTguMTI4NCAxMDEuMTc0IDE4LjEyODRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMDkuMDUzIDE1Ljc3NTlWMTcuMTg4NUgxMTAuMDc5VjE4LjE1NTNIMTA5LjA1M1YyMS4zOTk0QzEwOS4wNTMgMjEuNjIxNCAxMDkuMDk2IDIxLjc4MjYgMTA5LjE4MiAyMS44ODI4QzEwOS4yNzIgMjEuOTc5NSAxMDkuNDI5IDIyLjAyNzggMTA5LjY1NSAyMi4wMjc4QzEwOS44MDUgMjIuMDI3OCAxMDkuOTU3IDIyLjAwOTkgMTEwLjExMSAyMS45NzQxVjIyLjk4MzlDMTA5LjgxNCAyMy4wNjYyIDEwOS41MjggMjMuMTA3NCAxMDkuMjUyIDIzLjEwNzRDMTA4LjI0OSAyMy4xMDc0IDEwNy43NDggMjIuNTU0MiAxMDcuNzQ4IDIxLjQ0NzhWMTguMTU1M0gxMDYuNzkyVjE3LjE4ODVIMTA3Ljc0OFYxNS43NzU5SDEwOS4wNTNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMTMuMTE0IDIxLjEzMDlMMTE0LjI5NSAxNy4xODg1SDExNS42ODdMMTEzLjM3NyAyMy44ODA5QzExMy4wMjIgMjQuODU4NCAxMTIuNDIxIDI1LjM0NzIgMTExLjU3MiAyNS4zNDcyQzExMS4zODIgMjUuMzQ3MiAxMTEuMTczIDI1LjMxNDkgMTEwLjk0NCAyNS4yNTA1VjI0LjI0MDdMMTExLjE5MSAyNC4yNTY4QzExMS41MiAyNC4yNTY4IDExMS43NjcgMjQuMTk2IDExMS45MzIgMjQuMDc0MkMxMTIuMSAyMy45NTYxIDExMi4yMzMgMjMuNzU1NSAxMTIuMzMgMjMuNDcyN0wxMTIuNTE4IDIyLjk3MzFMMTEwLjQ3NyAxNy4xODg1SDExMS44ODRMMTEzLjExNCAyMS4xMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIxLjUzIDIwLjE1MzNDMTIxLjUzIDIxLjA1MjEgMTIxLjMyNiAyMS43NyAxMjAuOTE4IDIyLjMwNzFDMTIwLjUxIDIyLjg0MDcgMTE5Ljk2MiAyMy4xMDc0IDExOS4yNzQgMjMuMTA3NEMxMTguNjM3IDIzLjEwNzQgMTE4LjEyNyAyMi44OTc5IDExNy43NDQgMjIuNDc5VjI1LjIzNDRIMTE2LjQzOFYxNy4xODg1SDExNy42NDJMMTE3LjY5NSAxNy43NzkzQzExOC4wNzggMTcuMzEzOCAxMTguNTk5IDE3LjA4MTEgMTE5LjI1OCAxNy4wODExQzExOS45NjcgMTcuMDgxMSAxMjAuNTIyIDE3LjM0NiAxMjAuOTIzIDE3Ljg3NkMxMjEuMzI4IDE4LjQwMjMgMTIxLjUzIDE5LjEzNDYgMTIxLjUzIDIwLjA3MjhWMjAuMTUzM1pNMTIwLjIzIDIwLjA0MDVDMTIwLjIzIDE5LjQ2MDQgMTIwLjExNCAxOS4wMDAzIDExOS44ODEgMTguNjYwMkMxMTkuNjUyIDE4LjMyIDExOS4zMjMgMTguMTQ5OSAxMTguODkzIDE4LjE0OTlDMTE4LjM2IDE4LjE0OTkgMTE3Ljk3NiAxOC4zNzAxIDExNy43NDQgMTguODEwNVYyMS4zODg3QzExNy45OCAyMS44Mzk4IDExOC4zNjcgMjIuMDY1NCAxMTguOTA0IDIyLjA2NTRDMTE5LjMxOSAyMi4wNjU0IDExOS42NDMgMjEuODk4OSAxMTkuODc2IDIxLjU2NTlDMTIwLjExMiAyMS4yMjkzIDEyMC4yMyAyMC43MjA5IDEyMC4yMyAyMC4wNDA1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTI1LjE5OSAyMy4xMDc0QzEyNC4zNzIgMjMuMTA3NCAxMjMuNyAyMi44NDc4IDEyMy4xODUgMjIuMzI4NkMxMjIuNjczIDIxLjgwNTggMTIyLjQxNyAyMS4xMTEyIDEyMi40MTcgMjAuMjQ0NlYyMC4wODM1QzEyMi40MTcgMTkuNTAzNCAxMjIuNTI4IDE4Ljk4NiAxMjIuNzUgMTguNTMxMkMxMjIuOTc1IDE4LjA3MjkgMTIzLjI5IDE3LjcxNjYgMTIzLjY5NSAxNy40NjI0QzEyNC4wOTkgMTcuMjA4MiAxMjQuNTUxIDE3LjA4MTEgMTI1LjA0OCAxNy4wODExQzEyNS44NCAxNy4wODExIDEyNi40NSAxNy4zMzM1IDEyNi44OCAxNy44Mzg0QzEyNy4zMTMgMTguMzQzMyAxMjcuNTMgMTkuMDU3NiAxMjcuNTMgMTkuOTgxNFYyMC41MDc4SDEyMy43MzJDMTIzLjc3MiAyMC45ODc2IDEyMy45MzEgMjEuMzY3MiAxMjQuMjEgMjEuNjQ2NUMxMjQuNDkzIDIxLjkyNTggMTI0Ljg0OCAyMi4wNjU0IDEyNS4yNzQgMjIuMDY1NEMxMjUuODcyIDIyLjA2NTQgMTI2LjM1OSAyMS44MjM3IDEyNi43MzUgMjEuMzQwM0wxMjcuNDM4IDIyLjAxMTdDMTI3LjIwNiAyMi4zNTkgMTI2Ljg5NCAyMi42Mjk0IDEyNi41MDQgMjIuODIyOEMxMjYuMTE3IDIzLjAxMjUgMTI1LjY4MiAyMy4xMDc0IDEyNS4xOTkgMjMuMTA3NFpNMTI1LjA0MyAxOC4xMjg0QzEyNC42ODUgMTguMTI4NCAxMjQuMzk1IDE4LjI1MzcgMTI0LjE3MyAxOC41MDQ0QzEyMy45NTQgMTguNzU1IDEyMy44MTUgMTkuMTA0MiAxMjMuNzU0IDE5LjU1MThIMTI2LjI0MVYxOS40NTUxQzEyNi4yMTIgMTkuMDE4MiAxMjYuMDk2IDE4LjY4ODggMTI1Ljg5MiAxOC40NjY4QzEyNS42ODggMTguMjQxMiAxMjUuNDA1IDE4LjEyODQgMTI1LjA0MyAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ0Ljg4MiAyMC45ODU4QzE0NC44ODIgMjAuNjQyMSAxNDQuNzYxIDIwLjM3NzEgMTQ0LjUxNyAyMC4xOTA5QzE0NC4yNzcgMjAuMDA0NyAxNDMuODQyIDE5LjgxNjcgMTQzLjIxMiAxOS42MjdDMTQyLjU4MiAxOS40MzcyIDE0Mi4wOCAxOS4yMjU5IDE0MS43MDggMTguOTkzMkMxNDAuOTk1IDE4LjU0NTYgMTQwLjYzOSAxNy45NjE5IDE0MC42MzkgMTcuMjQyMkMxNDAuNjM5IDE2LjYxMiAxNDAuODk1IDE2LjA5MjggMTQxLjQwNyAxNS42ODQ2QzE0MS45MjMgMTUuMjc2NCAxNDIuNTkxIDE1LjA3MjMgMTQzLjQxMSAxNS4wNzIzQzE0My45NTUgMTUuMDcyMyAxNDQuNDQgMTUuMTcyNSAxNDQuODY2IDE1LjM3M0MxNDUuMjkyIDE1LjU3MzYgMTQ1LjYyNyAxNS44NiAxNDUuODcxIDE2LjIzMjRDMTQ2LjExNCAxNi42MDEyIDE0Ni4yMzYgMTcuMDExMiAxNDYuMjM2IDE3LjQ2MjRIMTQ0Ljg4MkMxNDQuODgyIDE3LjA1NDIgMTQ0Ljc1MyAxNi43MzU1IDE0NC40OTYgMTYuNTA2M0MxNDQuMjQxIDE2LjI3MzYgMTQzLjg3NiAxNi4xNTcyIDE0My40IDE2LjE1NzJDMTQyLjk1NiAxNi4xNTcyIDE0Mi42MSAxNi4yNTIxIDE0Mi4zNjMgMTYuNDQxOUMxNDIuMTIgMTYuNjMxNyAxNDEuOTk4IDE2Ljg5NjYgMTQxLjk5OCAxNy4yMzY4QzE0MS45OTggMTcuNTIzMyAxNDIuMTMxIDE3Ljc2MzIgMTQyLjM5NiAxNy45NTY1QzE0Mi42NiAxOC4xNDYzIDE0My4wOTcgMTguMzMyNSAxNDMuNzA2IDE4LjUxNTFDMTQ0LjMxNSAxOC42OTQyIDE0NC44MDQgMTguOTAwMSAxNDUuMTcyIDE5LjEzMjhDMTQ1LjU0MSAxOS4zNjIgMTQ1LjgxMiAxOS42MjcgMTQ1Ljk4MyAxOS45Mjc3QzE0Ni4xNTUgMjAuMjI0OSAxNDYuMjQxIDIwLjU3NDEgMTQ2LjI0MSAyMC45NzUxQzE0Ni4yNDEgMjEuNjI2OCAxNDUuOTkxIDIyLjE0NiAxNDUuNDg5IDIyLjUzMjdDMTQ0Ljk5MiAyMi45MTU5IDE0NC4zMTUgMjMuMTA3NCAxNDMuNDU5IDIzLjEwNzRDMTQyLjg5MyAyMy4xMDc0IDE0Mi4zNzIgMjMuMDAzNiAxNDEuODk2IDIyLjc5NTlDMTQxLjQyMyAyMi41ODQ2IDE0MS4wNTUgMjIuMjk0NiAxNDAuNzkgMjEuOTI1OEMxNDAuNTI4IDIxLjU1NyAxNDAuMzk3IDIxLjEyNzMgMTQwLjM5NyAyMC42MzY3SDE0MS43NTZDMTQxLjc1NiAyMS4wODA3IDE0MS45MDMgMjEuNDI0NSAxNDIuMTk3IDIxLjY2OEMxNDIuNDkgMjEuOTExNSAxNDIuOTExIDIyLjAzMzIgMTQzLjQ1OSAyMi4wMzMyQzE0My45MzIgMjIuMDMzMiAxNDQuMjg2IDIxLjkzODMgMTQ0LjUyMiAyMS43NDg1QzE0NC43NjIgMjEuNTU1MiAxNDQuODgyIDIxLjMwMDkgMTQ0Ljg4MiAyMC45ODU4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ4Ljk0MyAxNS43NzU5VjE3LjE4ODVIMTQ5Ljk2OVYxOC4xNTUzSDE0OC45NDNWMjEuMzk5NEMxNDguOTQzIDIxLjYyMTQgMTQ4Ljk4NiAyMS43ODI2IDE0OS4wNzIgMjEuODgyOEMxNDkuMTYxIDIxLjk3OTUgMTQ5LjMxOSAyMi4wMjc4IDE0OS41NDQgMjIuMDI3OEMxNDkuNjk1IDIyLjAyNzggMTQ5Ljg0NyAyMi4wMDk5IDE1MC4wMDEgMjEuOTc0MVYyMi45ODM5QzE0OS43MDQgMjMuMDY2MiAxNDkuNDE3IDIzLjEwNzQgMTQ5LjE0MiAyMy4xMDc0QzE0OC4xMzkgMjMuMTA3NCAxNDcuNjM4IDIyLjU1NDIgMTQ3LjYzOCAyMS40NDc4VjE4LjE1NTNIMTQ2LjY4MlYxNy4xODg1SDE0Ny42MzhWMTUuNzc1OUgxNDguOTQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTU0LjQ0MyAyM0MxNTQuMzg2IDIyLjg4OSAxNTQuMzM1IDIyLjcwODIgMTU0LjI5MiAyMi40NTc1QzE1My44NzcgMjIuODkwOCAxNTMuMzY5IDIzLjEwNzQgMTUyLjc2NyAyMy4xMDc0QzE1Mi4xODMgMjMuMTA3NCAxNTEuNzA3IDIyLjk0MDkgMTUxLjMzOCAyMi42MDc5QzE1MC45NyAyMi4yNzQ5IDE1MC43ODUgMjEuODYzMSAxNTAuNzg1IDIxLjM3MjZDMTUwLjc4NSAyMC43NTMxIDE1MS4wMTQgMjAuMjc4NiAxNTEuNDczIDE5Ljk0OTJDMTUxLjkzNSAxOS42MTYyIDE1Mi41OTMgMTkuNDQ5NyAxNTMuNDQ5IDE5LjQ0OTdIMTU0LjI1VjE5LjA2ODRDMTU0LjI1IDE4Ljc2NzYgMTU0LjE2NSAxOC41Mjc3IDE1My45OTcgMTguMzQ4NkMxNTMuODI5IDE4LjE2NiAxNTMuNTczIDE4LjA3NDcgMTUzLjIyOSAxOC4wNzQ3QzE1Mi45MzIgMTguMDc0NyAxNTIuNjg4IDE4LjE0OTkgMTUyLjQ5OSAxOC4zMDAzQzE1Mi4zMDkgMTguNDQ3MSAxNTIuMjE0IDE4LjYzNTEgMTUyLjIxNCAxOC44NjQzSDE1MC45MDlDMTUwLjkwOSAxOC41NDU2IDE1MS4wMTQgMTguMjQ4NCAxNTEuMjI2IDE3Ljk3MjdDMTUxLjQzNyAxNy42OTM0IDE1MS43MjMgMTcuNDc0OSAxNTIuMDg1IDE3LjMxNzRDMTUyLjQ1IDE3LjE1OTggMTUyLjg1NyAxNy4wODExIDE1My4zMDQgMTcuMDgxMUMxNTMuOTg1IDE3LjA4MTEgMTU0LjUyNyAxNy4yNTI5IDE1NC45MzIgMTcuNTk2N0MxNTUuMzM2IDE3LjkzNjggMTU1LjU0NCAxOC40MTY3IDE1NS41NTUgMTkuMDM2MVYyMS42NTcyQzE1NS41NTUgMjIuMTggMTU1LjYyOCAyMi41OTcyIDE1NS43NzUgMjIuOTA4N1YyM0gxNTQuNDQzWk0xNTMuMDA5IDIyLjA2MDFDMTUzLjI2NyAyMi4wNjAxIDE1My41MDggMjEuOTk3NCAxNTMuNzM0IDIxLjg3MjFDMTUzLjk2MyAyMS43NDY3IDE1NC4xMzUgMjEuNTc4NSAxNTQuMjUgMjEuMzY3MlYyMC4yNzE1SDE1My41NDZDMTUzLjA2MiAyMC4yNzE1IDE1Mi42OTkgMjAuMzU1NiAxNTIuNDU2IDIwLjUyMzlDMTUyLjIxMiAyMC42OTIyIDE1Mi4wOSAyMC45MzAzIDE1Mi4wOSAyMS4yMzgzQzE1Mi4wOSAyMS40ODg5IDE1Mi4xNzMgMjEuNjg5NSAxNTIuMzM3IDIxLjgzOThDMTUyLjUwNiAyMS45ODY3IDE1Mi43MjkgMjIuMDYwMSAxNTMuMDA5IDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNTguNTU3IDE1Ljc3NTlWMTcuMTg4NUgxNTkuNTgzVjE4LjE1NTNIMTU4LjU1N1YyMS4zOTk0QzE1OC41NTcgMjEuNjIxNCAxNTguNiAyMS43ODI2IDE1OC42ODYgMjEuODgyOEMxNTguNzc2IDIxLjk3OTUgMTU4LjkzMyAyMi4wMjc4IDE1OS4xNTkgMjIuMDI3OEMxNTkuMzA5IDIyLjAyNzggMTU5LjQ2MSAyMi4wMDk5IDE1OS42MTUgMjEuOTc0MVYyMi45ODM5QzE1OS4zMTggMjMuMDY2MiAxNTkuMDMyIDIzLjEwNzQgMTU4Ljc1NiAyMy4xMDc0QzE1Ny43NTMgMjMuMTA3NCAxNTcuMjUyIDIyLjU1NDIgMTU3LjI1MiAyMS40NDc4VjE4LjE1NTNIMTU2LjI5NlYxNy4xODg1SDE1Ny4yNTJWMTUuNzc1OUgxNTguNTU3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY0LjEwNSAyMi40MzA3QzE2My43MjIgMjIuODgxOCAxNjMuMTc4IDIzLjEwNzQgMTYyLjQ3MyAyMy4xMDc0QzE2MS44NDIgMjMuMTA3NCAxNjEuMzY0IDIyLjkyMyAxNjEuMDM5IDIyLjU1NDJDMTYwLjcxNiAyMi4xODU0IDE2MC41NTUgMjEuNjUxOSAxNjAuNTU1IDIwLjk1MzZWMTcuMTg4NUgxNjEuODZWMjAuOTM3NUMxNjEuODYgMjEuNjc1MSAxNjIuMTY3IDIyLjA0MzkgMTYyLjc3OSAyMi4wNDM5QzE2My40MTMgMjIuMDQzOSAxNjMuODQgMjEuODE2NiAxNjQuMDYyIDIxLjM2MThWMTcuMTg4NUgxNjUuMzY4VjIzSDE2NC4xMzhMMTY0LjEwNSAyMi40MzA3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY5Ljk1NSAyMS40MjA5QzE2OS45NTUgMjEuMTg4MiAxNjkuODU4IDIxLjAxMDkgMTY5LjY2NSAyMC44ODkyQzE2OS40NzUgMjAuNzY3NCAxNjkuMTU4IDIwLjY2IDE2OC43MTQgMjAuNTY2OUMxNjguMjcgMjAuNDczOCAxNjcuODk5IDIwLjM1NTYgMTY3LjYwMiAyMC4yMTI0QzE2Ni45NSAxOS44OTczIDE2Ni42MjUgMTkuNDQwOCAxNjYuNjI1IDE4Ljg0MjhDMTY2LjYyNSAxOC4zNDE1IDE2Ni44MzYgMTcuOTIyNSAxNjcuMjU4IDE3LjU4NTlDMTY3LjY4MSAxNy4yNDkzIDE2OC4yMTggMTcuMDgxMSAxNjguODcgMTcuMDgxMUMxNjkuNTY0IDE3LjA4MTEgMTcwLjEyNSAxNy4yNTI5IDE3MC41NTEgMTcuNTk2N0MxNzAuOTggMTcuOTQwNCAxNzEuMTk1IDE4LjM4NjIgMTcxLjE5NSAxOC45MzQxSDE2OS44OUMxNjkuODkgMTguNjgzNCAxNjkuNzk3IDE4LjQ3NTcgMTY5LjYxMSAxOC4zMTFDMTY5LjQyNSAxOC4xNDI3IDE2OS4xNzggMTguMDU4NiAxNjguODcgMTguMDU4NkMxNjguNTgzIDE4LjA1ODYgMTY4LjM0OSAxOC4xMjQ4IDE2OC4xNjYgMTguMjU3M0MxNjcuOTg3IDE4LjM4OTggMTY3Ljg5NyAxOC41NjcxIDE2Ny44OTcgMTguNzg5MUMxNjcuODk3IDE4Ljk4OTYgMTY3Ljk4MiAxOS4xNDUzIDE2OC4xNSAxOS4yNTYzQzE2OC4zMTggMTkuMzY3NCAxNjguNjU4IDE5LjQ4MDEgMTY5LjE3IDE5LjU5NDdDMTY5LjY4MiAxOS43MDU3IDE3MC4wODMgMTkuODQgMTcwLjM3NCAxOS45OTc2QzE3MC42NjcgMjAuMTUxNSAxNzAuODg0IDIwLjMzNzcgMTcxLjAyMyAyMC41NTYyQzE3MS4xNjcgMjAuNzc0NiAxNzEuMjM4IDIxLjAzOTYgMTcxLjIzOCAyMS4zNTExQzE3MS4yMzggMjEuODczOSAxNzEuMDIyIDIyLjI5ODIgMTcwLjU4OCAyMi42MjRDMTcwLjE1NSAyMi45NDYzIDE2OS41ODggMjMuMTA3NCAxNjguODg2IDIzLjEwNzRDMTY4LjQxIDIzLjEwNzQgMTY3Ljk4NSAyMy4wMjE1IDE2Ny42MTMgMjIuODQ5NkMxNjcuMjQgMjIuNjc3NyAxNjYuOTUgMjIuNDQxNCAxNjYuNzQzIDIyLjE0MDZDMTY2LjUzNSAyMS44Mzk4IDE2Ni40MzEgMjEuNTE1OCAxNjYuNDMxIDIxLjE2ODVIMTY3LjY5OUMxNjcuNzE3IDIxLjQ3NjQgMTY3LjgzMyAyMS43MTQ1IDE2OC4wNDggMjEuODgyOEMxNjguMjYzIDIyLjA0NzUgMTY4LjU0NyAyMi4xMjk5IDE2OC45MDIgMjIuMTI5OUMxNjkuMjQ2IDIyLjEyOTkgMTY5LjUwNyAyMi4wNjU0IDE2OS42ODYgMjEuOTM2NUMxNjkuODY1IDIxLjgwNCAxNjkuOTU1IDIxLjYzMjIgMTY5Ljk1NSAyMS40MjA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCA2My40NzlDMTIuNzczNCA2My4zMDcxIDEyLjgyMzYgNjMuMTYzOSAxMi45MjM4IDYzLjA0OTNDMTMuMDI3NyA2Mi45MzQ3IDEzLjE4MTYgNjIuODc3NCAxMy4zODU3IDYyLjg3NzRDMTMuNTg5OCA2Mi44Nzc0IDEzLjc0MzggNjIuOTM0NyAxMy44NDc3IDYzLjA0OTNDMTMuOTU1MSA2My4xNjM5IDE0LjAwODggNjMuMzA3MSAxNC4wMDg4IDYzLjQ3OUMxNC4wMDg4IDYzLjY0MzcgMTMuOTU1MSA2My43ODE2IDEzLjg0NzcgNjMuODkyNkMxMy43NDM4IDY0LjAwMzYgMTMuNTg5OCA2NC4wNTkxIDEzLjM4NTcgNjQuMDU5MUMxMy4xODE2IDY0LjA1OTEgMTMuMDI3NyA2NC4wMDM2IDEyLjkyMzggNjMuODkyNkMxMi44MjM2IDYzLjc4MTYgMTIuNzczNCA2My42NDM3IDEyLjc3MzQgNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCA2My40NzlDMTUuNjczOCA2My4zMDcxIDE1LjcyNCA2My4xNjM5IDE1LjgyNDIgNjMuMDQ5M0MxNS45MjgxIDYyLjkzNDcgMTYuMDgyIDYyLjg3NzQgMTYuMjg2MSA2Mi44Nzc0QzE2LjQ5MDIgNjIuODc3NCAxNi42NDQyIDYyLjkzNDcgMTYuNzQ4IDYzLjA0OTNDMTYuODU1NSA2My4xNjM5IDE2LjkwOTIgNjMuMzA3MSAxNi45MDkyIDYzLjQ3OUMxNi45MDkyIDYzLjY0MzcgMTYuODU1NSA2My43ODE2IDE2Ljc0OCA2My44OTI2QzE2LjY0NDIgNjQuMDAzNiAxNi40OTAyIDY0LjA1OTEgMTYuMjg2MSA2NC4wNTkxQzE2LjA4MiA2NC4wNTkxIDE1LjkyODEgNjQuMDAzNiAxNS44MjQyIDYzLjg5MjZDMTUuNzI0IDYzLjc4MTYgMTUuNjczOCA2My42NDM3IDE1LjY3MzggNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjAuODgzOCA2My4yOTY0QzIxLjIzODMgNjMuMjk2NCAyMS41NDggNjMuMTg5IDIxLjgxMyA2Mi45NzQxQzIyLjA3OCA2Mi43NTkzIDIyLjIyNDggNjIuNDkwNyAyMi4yNTM0IDYyLjE2ODVIMjMuMTkzNEMyMy4xNzU1IDYyLjUwMTUgMjMuMDYwOSA2Mi44MTg0IDIyLjg0OTYgNjMuMTE5MUMyMi42MzgzIDYzLjQxOTkgMjIuMzU1NSA2My42NTk4IDIyLjAwMSA2My44Mzg5QzIxLjY1MDEgNjQuMDE3OSAyMS4yNzc3IDY0LjEwNzQgMjAuODgzOCA2NC4xMDc0QzIwLjA5MjQgNjQuMTA3NCAxOS40NjIyIDYzLjg0NDIgMTguOTkzMiA2My4zMTc5QzE4LjUyNzcgNjIuNzg3OSAxOC4yOTQ5IDYyLjA2NDYgMTguMjk0OSA2MS4xNDc5VjYwLjk4MTRDMTguMjk0OSA2MC40MTU3IDE4LjM5ODggNTkuOTEyNiAxOC42MDY0IDU5LjQ3MjJDMTguODE0MSA1OS4wMzE3IDE5LjExMTMgNTguNjg5OCAxOS40OTggNTguNDQ2M0MxOS44ODgzIDU4LjIwMjggMjAuMzQ4NSA1OC4wODExIDIwLjg3ODQgNTguMDgxMUMyMS41MzAxIDU4LjA4MTEgMjIuMDcwOCA1OC4yNzYyIDIyLjUwMDUgNTguNjY2NUMyMi45MzM4IDU5LjA1NjggMjMuMTY0NyA1OS41NjM1IDIzLjE5MzQgNjAuMTg2NUgyMi4yNTM0QzIyLjIyNDggNTkuODEwNSAyMi4wODE1IDU5LjUwMjYgMjEuODIzNyA1OS4yNjI3QzIxLjU2OTUgNTkuMDE5MiAyMS4yNTQ0IDU4Ljg5NzUgMjAuODc4NCA1OC44OTc1QzIwLjM3MzUgNTguODk3NSAxOS45ODE0IDU5LjA4MDEgMTkuNzAyMSA1OS40NDUzQzE5LjQyNjQgNTkuODA3IDE5LjI4ODYgNjAuMzMxNSAxOS4yODg2IDYxLjAxOVY2MS4yMDdDMTkuMjg4NiA2MS44NzY2IDE5LjQyNjQgNjIuMzkyMyAxOS43MDIxIDYyLjc1MzlDMTkuOTc3OSA2My4xMTU2IDIwLjM3MTcgNjMuMjk2NCAyMC44ODM4IDYzLjI5NjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yNC4wNDc0IDYxLjA0MDVDMjQuMDQ3NCA2MC40NzEyIDI0LjE1ODQgNTkuOTU5MSAyNC4zODA0IDU5LjUwNDRDMjQuNjA2IDU5LjA0OTYgMjQuOTE3NSA1OC42OTg3IDI1LjMxNDkgNTguNDUxN0MyNS43MTYgNTguMjA0NiAyNi4xNzI1IDU4LjA4MTEgMjYuNjg0NiA1OC4wODExQzI3LjQ3NTkgNTguMDgxMSAyOC4xMTUxIDU4LjM1NSAyOC42MDIxIDU4LjkwMjhDMjkuMDkyNiA1OS40NTA3IDI5LjMzNzkgNjAuMTc5NCAyOS4zMzc5IDYxLjA4ODlWNjEuMTU4N0MyOS4zMzc5IDYxLjcyNDQgMjkuMjI4NyA2Mi4yMzI5IDI5LjAxMDMgNjIuNjg0MUMyOC43OTU0IDYzLjEzMTcgMjguNDg1NyA2My40ODA4IDI4LjA4MTEgNjMuNzMxNEMyNy42OCA2My45ODIxIDI3LjIxODEgNjQuMTA3NCAyNi42OTUzIDY0LjEwNzRDMjUuOTA3NiA2NC4xMDc0IDI1LjI2ODQgNjMuODMzNSAyNC43Nzc4IDYzLjI4NTZDMjQuMjkwOSA2Mi43Mzc4IDI0LjA0NzQgNjIuMDEyNyAyNC4wNDc0IDYxLjExMDRWNjEuMDQwNVpNMjUuMDQ2NCA2MS4xNTg3QzI1LjA0NjQgNjEuODAzMiAyNS4xOTUgNjIuMzIwNiAyNS40OTIyIDYyLjcxMDlDMjUuNzkzIDYzLjEwMTIgMjYuMTk0IDYzLjI5NjQgMjYuNjk1MyA2My4yOTY0QzI3LjIwMDIgNjMuMjk2NCAyNy42MDEyIDYzLjA5OTQgMjcuODk4NCA2Mi43MDU2QzI4LjE5NTYgNjIuMzA4MSAyOC4zNDQyIDYxLjc1MzEgMjguMzQ0MiA2MS4wNDA1QzI4LjM0NDIgNjAuNDAzMiAyOC4xOTIxIDU5Ljg4NzUgMjcuODg3NyA1OS40OTM3QzI3LjU4NjkgNTkuMDk2MiAyNy4xODU5IDU4Ljg5NzUgMjYuNjg0NiA1OC44OTc1QzI2LjE5NCA1OC44OTc1IDI1Ljc5ODMgNTkuMDkyNiAyNS40OTc2IDU5LjQ4MjlDMjUuMTk2OCA1OS44NzMyIDI1LjA0NjQgNjAuNDMxOCAyNS4wNDY0IDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNC42NzY4IDYxLjM3MzVIMzUuNzYxN1Y2Mi4xODQ2SDM0LjY3NjhWNjRIMzMuNjc3N1Y2Mi4xODQ2SDMwLjExNjdWNjEuNTk5MUwzMy42MTg3IDU2LjE3OTdIMzQuNjc2OFY2MS4zNzM1Wk0zMS4yNDQ2IDYxLjM3MzVIMzMuNjc3N1Y1Ny41Mzg2TDMzLjU1OTYgNTcuNzUzNEwzMS4yNDQ2IDYxLjM3MzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNy43ODY2IDU4LjE4ODVWNjQuNjcxNEMzNy43ODY2IDY1Ljc4ODYgMzcuMjc5OSA2Ni4zNDcyIDM2LjI2NjYgNjYuMzQ3MkMzNi4wNDgyIDY2LjM0NzIgMzUuODQ1OSA2Ni4zMTQ5IDM1LjY1OTcgNjYuMjUwNVY2NS40NTU2QzM1Ljc3NDMgNjUuNDg0MiAzNS45MjQ2IDY1LjQ5ODUgMzYuMTEwOCA2NS40OTg1QzM2LjMzMjggNjUuNDk4NSAzNi41MDExIDY1LjQzNzcgMzYuNjE1NyA2NS4zMTU5QzM2LjczMzkgNjUuMTk3OCAzNi43OTMgNjQuOTkwMSAzNi43OTMgNjQuNjkyOVY1OC4xODg1SDM3Ljc4NjZaTTM2LjY5MDkgNTYuNjQ3QzM2LjY5MDkgNTYuNDg5NCAzNi43MzkzIDU2LjM1NTEgMzYuODM1OSA1Ni4yNDQxQzM2LjkzNjIgNTYuMTI5NiAzNy4wODEyIDU2LjA3MjMgMzcuMjcxIDU2LjA3MjNDMzcuNDY0NCA1Ni4wNzIzIDM3LjYxMTIgNTYuMTI3OCAzNy43MTE0IDU2LjIzODhDMzcuODExNyA1Ni4zNDk4IDM3Ljg2MTggNTYuNDg1OCAzNy44NjE4IDU2LjY0N0MzNy44NjE4IDU2LjgwODEgMzcuODExNyA1Ni45NDI0IDM3LjcxMTQgNTcuMDQ5OEMzNy42MTEyIDU3LjE1NzIgMzcuNDY0NCA1Ny4yMTA5IDM3LjI3MSA1Ny4yMTA5QzM3LjA3NzYgNTcuMjEwOSAzNi45MzI2IDU3LjE1NzIgMzYuODM1OSA1Ny4wNDk4QzM2LjczOTMgNTYuOTQyNCAzNi42OTA5IDU2LjgwODEgMzYuNjkwOSA1Ni42NDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00Mi45ODA1IDYzLjQyNTNDNDIuNTkzOCA2My44OCA0Mi4wMjYyIDY0LjEwNzQgNDEuMjc3OCA2NC4xMDc0QzQwLjY1ODQgNjQuMTA3NCA0MC4xODU3IDYzLjkyODQgMzkuODU5OSA2My41NzAzQzM5LjUzNzYgNjMuMjA4NyAzOS4zNzQ3IDYyLjY3NTEgMzkuMzcxMSA2MS45Njk3VjU4LjE4ODVINDAuMzY0N1Y2MS45NDI5QzQwLjM2NDcgNjIuODIzNyA0MC43MjI4IDYzLjI2NDIgNDEuNDM5IDYzLjI2NDJDNDIuMTk4MSA2My4yNjQyIDQyLjcwMyA2Mi45ODEzIDQyLjk1MzYgNjIuNDE1NVY1OC4xODg1SDQzLjk0NzNWNjRINDMuMDAyTDQyLjk4MDUgNjMuNDI1M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjMxMSA1Ny4wMjgzSDYyLjc5NzRWNjRINjEuNzcxNVY1Ny4wMjgzSDU5LjI2MzJWNTYuMTc5N0g2NS4zMTFWNTcuMDI4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTcxLjE0OTQgNjIuNjMwNEw3Mi4yNjY2IDU4LjE4ODVINzMuMjYwM0w3MS41Njg0IDY0SDcwLjc2MjdMNjkuMzUwMSA1OS41OTU3TDY3Ljk3NTEgNjRINjcuMTY5NEw2NS40ODI5IDU4LjE4ODVINjYuNDcxMkw2Ny42MTUyIDYyLjUzOTFMNjguOTY4OCA1OC4xODg1SDY5Ljc2OUw3MS4xNDk0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NC4wMTIyIDYxLjA0MDVDNzQuMDEyMiA2MC40NzEyIDc0LjEyMzIgNTkuOTU5MSA3NC4zNDUyIDU5LjUwNDRDNzQuNTcwOCA1OS4wNDk2IDc0Ljg4MjMgNTguNjk4NyA3NS4yNzk4IDU4LjQ1MTdDNzUuNjgwOCA1OC4yMDQ2IDc2LjEzNzQgNTguMDgxMSA3Ni42NDk0IDU4LjA4MTFDNzcuNDQwOCA1OC4wODExIDc4LjA3OTkgNTguMzU1IDc4LjU2NjkgNTguOTAyOEM3OS4wNTc1IDU5LjQ1MDcgNzkuMzAyNyA2MC4xNzk0IDc5LjMwMjcgNjEuMDg4OVY2MS4xNTg3Qzc5LjMwMjcgNjEuNzI0NCA3OS4xOTM1IDYyLjIzMjkgNzguOTc1MSA2Mi42ODQxQzc4Ljc2MDMgNjMuMTMxNyA3OC40NTA1IDYzLjQ4MDggNzguMDQ1OSA2My43MzE0Qzc3LjY0NDkgNjMuOTgyMSA3Ny4xODI5IDY0LjEwNzQgNzYuNjYwMiA2NC4xMDc0Qzc1Ljg3MjQgNjQuMTA3NCA3NS4yMzMyIDYzLjgzMzUgNzQuNzQyNyA2My4yODU2Qzc0LjI1NTcgNjIuNzM3OCA3NC4wMTIyIDYyLjAxMjcgNzQuMDEyMiA2MS4xMTA0VjYxLjA0MDVaTTc1LjAxMTIgNjEuMTU4N0M3NS4wMTEyIDYxLjgwMzIgNzUuMTU5OCA2Mi4zMjA2IDc1LjQ1NyA2Mi43MTA5Qzc1Ljc1NzggNjMuMTAxMiA3Ni4xNTg5IDYzLjI5NjQgNzYuNjYwMiA2My4yOTY0Qzc3LjE2NSA2My4yOTY0IDc3LjU2NjEgNjMuMDk5NCA3Ny44NjMzIDYyLjcwNTZDNzguMTYwNSA2Mi4zMDgxIDc4LjMwOTEgNjEuNzUzMSA3OC4zMDkxIDYxLjA0MDVDNzguMzA5MSA2MC40MDMyIDc4LjE1NjkgNTkuODg3NSA3Ny44NTI1IDU5LjQ5MzdDNzcuNTUxOCA1OS4wOTYyIDc3LjE1MDcgNTguODk3NSA3Ni42NDk0IDU4Ljg5NzVDNzYuMTU4OSA1OC44OTc1IDc1Ljc2MzIgNTkuMDkyNiA3NS40NjI0IDU5LjQ4MjlDNzUuMTYxNiA1OS44NzMyIDc1LjAxMTIgNjAuNDMxOCA3NS4wMTEyIDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04Mi42MTY3IDYxLjA4MzVINzkuOTk1NlY2MC4yNzI1SDgyLjYxNjdWNjEuMDgzNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg4LjczNDQgNjIuNjMwNEw4OS44NTE2IDU4LjE4ODVIOTAuODQ1Mkw4OS4xNTMzIDY0SDg4LjM0NzdMODYuOTM1MSA1OS41OTU3TDg1LjU2MDEgNjRIODQuNzU0NEw4My4wNjc5IDU4LjE4ODVIODQuMDU2Mkw4NS4yMDAyIDYyLjUzOTFMODYuNTUzNyA1OC4xODg1SDg3LjM1NEw4OC43MzQ0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NS40NDgyIDY0Qzk1LjM5MSA2My44ODU0IDk1LjM0NDQgNjMuNjgxMyA5NS4zMDg2IDYzLjM4NzdDOTQuODQ2NyA2My44Njc1IDk0LjI5NTIgNjQuMTA3NCA5My42NTQzIDY0LjEwNzRDOTMuMDgxNCA2NC4xMDc0IDkyLjYxMDUgNjMuOTQ2MyA5Mi4yNDE3IDYzLjYyNEM5MS44NzY1IDYzLjI5ODIgOTEuNjkzOCA2Mi44ODY0IDkxLjY5MzggNjIuMzg4N0M5MS42OTM4IDYxLjc4MzUgOTEuOTIzIDYxLjMxNDUgOTIuMzgxMyA2MC45ODE0QzkyLjg0MzMgNjAuNjQ0OSA5My40OTE0IDYwLjQ3NjYgOTQuMzI1NyA2MC40NzY2SDk1LjI5MjVWNjAuMDJDOTUuMjkyNSA1OS42NzI3IDk1LjE4ODYgNTkuMzk3IDk0Ljk4MSA1OS4xOTI5Qzk0Ljc3MzMgNTguOTg1MiA5NC40NjcxIDU4Ljg4MTMgOTQuMDYyNSA1OC44ODEzQzkzLjcwOCA1OC44ODEzIDkzLjQxMDggNTguOTcwOSA5My4xNzA5IDU5LjE0OTlDOTIuOTMxIDU5LjMyODkgOTIuODExIDU5LjU0NTYgOTIuODExIDU5Ljc5OThIOTEuODEyQzkxLjgxMiA1OS41MDk4IDkxLjkxNDEgNTkuMjMwNSA5Mi4xMTgyIDU4Ljk2MTlDOTIuMzI1OCA1OC42ODk4IDkyLjYwNTEgNTguNDc0OSA5Mi45NTYxIDU4LjMxNzRDOTMuMzEwNSA1OC4xNTk4IDkzLjY5OTEgNTguMDgxMSA5NC4xMjE2IDU4LjA4MTFDOTQuNzkxMiA1OC4wODExIDk1LjMxNTggNTguMjQ5MyA5NS42OTUzIDU4LjU4NTlDOTYuMDc0OSA1OC45MTg5IDk2LjI3MTggNTkuMzc5MSA5Ni4yODYxIDU5Ljk2NjNWNjIuNjQxMUM5Ni4yODYxIDYzLjE3NDYgOTYuMzU0MiA2My41OTkgOTYuNDkwMiA2My45MTQxVjY0SDk1LjQ0ODJaTTkzLjc5OTMgNjMuMjQyN0M5NC4xMTA4IDYzLjI0MjcgOTQuNDA2MiA2My4xNjIxIDk0LjY4NTUgNjMuMDAxQzk0Ljk2NDggNjIuODM5OCA5NS4xNjcyIDYyLjYzMDQgOTUuMjkyNSA2Mi4zNzI2VjYxLjE4MDJIOTQuNTEzN0M5My4yOTYyIDYxLjE4MDIgOTIuNjg3NSA2MS41MzY1IDkyLjY4NzUgNjIuMjQ5QzkyLjY4NzUgNjIuNTYwNSA5Mi43OTEzIDYyLjgwNCA5Mi45OTkgNjIuOTc5NUM5My4yMDY3IDYzLjE1NDkgOTMuNDczNSA2My4yNDI3IDkzLjc5OTMgNjMuMjQyN1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTk5LjY1OTIgNjIuNTQ0NEwxMDEuMDEzIDU4LjE4ODVIMTAyLjA3Nkw5OS43Mzk3IDY0Ljg5N0M5OS4zNzgxIDY1Ljg2MzggOTguODAzNCA2Ni4zNDcyIDk4LjAxNTYgNjYuMzQ3Mkw5Ny44Mjc2IDY2LjMzMTFMOTcuNDU3IDY2LjI2MTJWNjUuNDU1Nkw5Ny43MjU2IDY1LjQ3NzFDOTguMDYyMiA2NS40NzcxIDk4LjMyMzYgNjUuNDA5IDk4LjUwOTggNjUuMjcyOUM5OC42OTk1IDY1LjEzNjkgOTguODU1MyA2NC44ODggOTguOTc3MSA2NC41MjY0TDk5LjE5NzMgNjMuOTM1NUw5Ny4xMjQgNTguMTg4NUg5OC4yMDlMOTkuNjU5MiA2Mi41NDQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNjUuOTIzMyAxMDAuMzQyQzY1LjkyMzMgMTAxLjEwOSA2NS43OTQ0IDEwMS43NzggNjUuNTM2NiAxMDIuMzUxQzY1LjI3ODggMTAyLjkyIDY0LjkxMzYgMTAzLjM1NSA2NC40NDA5IDEwMy42NTZDNjMuOTY4MyAxMDMuOTU3IDYzLjQxNjggMTA0LjEwNyA2Mi43ODY2IDEwNC4xMDdDNjIuMTcwNyAxMDQuMTA3IDYxLjYyNDcgMTAzLjk1NyA2MS4xNDg0IDEwMy42NTZDNjAuNjcyMiAxMDMuMzUyIDYwLjMwMTYgMTAyLjkyIDYwLjAzNjYgMTAyLjM2MkM1OS43NzUyIDEwMS44IDU5LjY0MSAxMDEuMTUgNTkuNjMzOCAxMDAuNDEyVjk5Ljg0ODFDNTkuNjMzOCA5OS4wOTYyIDU5Ljc2NDUgOTguNDMyIDYwLjAyNTkgOTcuODU1NUM2MC4yODczIDk3LjI3OSA2MC42NTYxIDk2LjgzODUgNjEuMTMyMyA5Ni41MzQyQzYxLjYxMjEgOTYuMjI2MiA2Mi4xNiA5Ni4wNzIzIDYyLjc3NTkgOTYuMDcyM0M2My40MDI1IDk2LjA3MjMgNjMuOTUzOSA5Ni4yMjQ0IDY0LjQzMDIgOTYuNTI4OEM2NC45MSA5Ni44Mjk2IDY1LjI3ODggOTcuMjY4MiA2NS41MzY2IDk3Ljg0NDdDNjUuNzk0NCA5OC40MTc2IDY1LjkyMzMgOTkuMDg1NCA2NS45MjMzIDk5Ljg0ODFWMTAwLjM0MlpNNjQuODk3NSA5OS44Mzc0QzY0Ljg5NzUgOTguOTEgNjQuNzExMyA5OC4xOTkyIDY0LjMzODkgOTcuNzA1MUM2My45NjY1IDk3LjIwNzQgNjMuNDQ1NSA5Ni45NTg1IDYyLjc3NTkgOTYuOTU4NUM2Mi4xMjQyIDk2Ljk1ODUgNjEuNjEwNCA5Ny4yMDc0IDYxLjIzNDQgOTcuNzA1MUM2MC44NjIgOTguMTk5MiA2MC42NzA0IDk4Ljg4NjcgNjAuNjU5NyA5OS43Njc2VjEwMC4zNDJDNjAuNjU5NyAxMDEuMjQxIDYwLjg0NzcgMTAxLjk0OCA2MS4yMjM2IDEwMi40NjRDNjEuNjAzMiAxMDIuOTc2IDYyLjEyNDIgMTAzLjIzMiA2Mi43ODY2IDEwMy4yMzJDNjMuNDUyNiAxMDMuMjMyIDYzLjk2ODMgMTAyLjk5IDY0LjMzMzUgMTAyLjUwN0M2NC42OTg3IDEwMi4wMiA2NC44ODY3IDEwMS4zMjMgNjQuODk3NSAxMDAuNDE3Vjk5LjgzNzRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDk4LjE4ODVMNjguMjg2NiA5OC45MTg5QzY4LjczMDYgOTguMzYwNCA2OS4zMTA3IDk4LjA4MTEgNzAuMDI2OSA5OC4wODExQzcxLjI1NSA5OC4wODExIDcxLjg3NDUgOTguNzczOSA3MS44ODUzIDEwMC4xNlYxMDRINzAuODkxNlYxMDAuMTU0QzcwLjg4OCA5OS43MzU0IDcwLjc5MTMgOTkuNDI1NiA3MC42MDE2IDk5LjIyNTFDNzAuNDE1NCA5OS4wMjQ2IDcwLjEyMzUgOTguOTI0MyA2OS43MjYxIDk4LjkyNDNDNjkuNDAzOCA5OC45MjQzIDY5LjEyMDkgOTkuMDEwMyA2OC44Nzc0IDk5LjE4MjFDNjguNjM0IDk5LjM1NCA2OC40NDQyIDk5LjU3OTYgNjguMzA4MSA5OS44NTg5VjEwNEg2Ny4zMTQ1Vjk4LjE4ODVINjguMjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTc1Ljc5NTQgMTA0LjEwN0M3NS4wMDc2IDEwNC4xMDcgNzQuMzY2NyAxMDMuODUgNzMuODcyNiAxMDMuMzM0QzczLjM3ODQgMTAyLjgxNSA3My4xMzEzIDEwMi4xMjIgNzMuMTMxMyAxMDEuMjU1VjEwMS4wNzNDNzMuMTMxMyAxMDAuNDk2IDczLjI0MDYgOTkuOTgyNCA3My40NTkgOTkuNTMxMkM3My42ODEgOTkuMDc2NSA3My45ODg5IDk4LjcyMiA3NC4zODI4IDk4LjQ2NzhDNzQuNzgwMyA5OC4yMSA3NS4yMSA5OC4wODExIDc1LjY3MTkgOTguMDgxMUM3Ni40Mjc0IDk4LjA4MTEgNzcuMDE0NiA5OC4zMjk5IDc3LjQzMzYgOTguODI3NkM3Ny44NTI1IDk5LjMyNTQgNzguMDYyIDEwMC4wMzggNzguMDYyIDEwMC45NjVWMTAxLjM3OUg3NC4xMjVDNzQuMTM5MyAxMDEuOTUyIDc0LjMwNTggMTAyLjQxNiA3NC42MjQ1IDEwMi43N0M3NC45NDY4IDEwMy4xMjEgNzUuMzU1IDEwMy4yOTYgNzUuODQ5MSAxMDMuMjk2Qzc2LjIgMTAzLjI5NiA3Ni40OTcyIDEwMy4yMjUgNzYuNzQwNyAxMDMuMDgyQzc2Ljk4NDIgMTAyLjkzOCA3Ny4xOTczIDEwMi43NDkgNzcuMzc5OSAxMDIuNTEyTDc3Ljk4NjggMTAyLjk4NUM3Ny40OTk4IDEwMy43MzMgNzYuNzY5NCAxMDQuMTA3IDc1Ljc5NTQgMTA0LjEwN1pNNzUuNjcxOSA5OC44OTc1Qzc1LjI3MDggOTguODk3NSA3NC45MzQyIDk5LjA0NDMgNzQuNjYyMSA5OS4zMzc5Qzc0LjM5IDk5LjYyNzkgNzQuMjIxNyAxMDAuMDM2IDc0LjE1NzIgMTAwLjU2Mkg3Ny4wNjg0VjEwMC40ODdDNzcuMDM5NyA5OS45ODI0IDc2LjkwMzYgOTkuNTkyMSA3Ni42NjAyIDk5LjMxNjRDNzYuNDE2NyA5OS4wMzcxIDc2LjA4NzIgOTguODk3NSA3NS42NzE5IDk4Ljg5NzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04MS4yODQ3IDEwMS4wODNINzguNjYzNlYxMDAuMjcySDgxLjI4NDdWMTAxLjA4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg3LjQwMjMgMTAyLjYzTDg4LjUxOTUgOTguMTg4NUg4OS41MTMyTDg3LjgyMTMgMTA0SDg3LjAxNTZMODUuNjAzIDk5LjU5NTdMODQuMjI4IDEwNEg4My40MjI0TDgxLjczNTggOTguMTg4NUg4Mi43MjQxTDgzLjg2ODIgMTAyLjUzOUw4NS4yMjE3IDk4LjE4ODVIODYuMDIyTDg3LjQwMjMgMTAyLjYzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTQuMTE2MiAxMDRDOTQuMDU4OSAxMDMuODg1IDk0LjAxMjQgMTAzLjY4MSA5My45NzY2IDEwMy4zODhDOTMuNTE0NiAxMDMuODY4IDkyLjk2MzIgMTA0LjEwNyA5Mi4zMjIzIDEwNC4xMDdDOTEuNzQ5MyAxMDQuMTA3IDkxLjI3ODUgMTAzLjk0NiA5MC45MDk3IDEwMy42MjRDOTAuNTQ0NCAxMDMuMjk4IDkwLjM2MTggMTAyLjg4NiA5MC4zNjE4IDEwMi4zODlDOTAuMzYxOCAxMDEuNzg0IDkwLjU5MSAxMDEuMzE0IDkxLjA0OTMgMTAwLjk4MUM5MS41MTEyIDEwMC42NDUgOTIuMTU5MyAxMDAuNDc3IDkyLjk5MzcgMTAwLjQ3N0g5My45NjA0VjEwMC4wMkM5My45NjA0IDk5LjY3MjcgOTMuODU2NiA5OS4zOTcgOTMuNjQ4OSA5OS4xOTI5QzkzLjQ0MTIgOTguOTg1MiA5My4xMzUxIDk4Ljg4MTMgOTIuNzMwNSA5OC44ODEzQzkyLjM3NiA5OC44ODEzIDkyLjA3ODggOTguOTcwOSA5MS44Mzg5IDk5LjE0OTlDOTEuNTk5IDk5LjMyODkgOTEuNDc5IDk5LjU0NTYgOTEuNDc5IDk5Ljc5OThIOTAuNDhDOTAuNDggOTkuNTA5OCA5MC41ODIgOTkuMjMwNSA5MC43ODYxIDk4Ljk2MTlDOTAuOTkzOCA5OC42ODk4IDkxLjI3MzEgOTguNDc0OSA5MS42MjQgOTguMzE3NEM5MS45Nzg1IDk4LjE1OTggOTIuMzY3IDk4LjA4MTEgOTIuNzg5NiA5OC4wODExQzkzLjQ1OTEgOTguMDgxMSA5My45ODM3IDk4LjI0OTMgOTQuMzYzMyA5OC41ODU5Qzk0Ljc0MjggOTguOTE4OSA5NC45Mzk4IDk5LjM3OTEgOTQuOTU0MSA5OS45NjYzVjEwMi42NDFDOTQuOTU0MSAxMDMuMTc1IDk1LjAyMjEgMTAzLjU5OSA5NS4xNTgyIDEwMy45MTRWMTA0SDk0LjExNjJaTTkyLjQ2NzMgMTAzLjI0M0M5Mi43Nzg4IDEwMy4yNDMgOTMuMDc0MiAxMDMuMTYyIDkzLjM1MzUgMTAzLjAwMUM5My42MzI4IDEwMi44NCA5My44MzUxIDEwMi42MyA5My45NjA0IDEwMi4zNzNWMTAxLjE4SDkzLjE4MTZDOTEuOTY0MiAxMDEuMTggOTEuMzU1NSAxMDEuNTM2IDkxLjM1NTUgMTAyLjI0OUM5MS4zNTU1IDEwMi41NjEgOTEuNDU5MyAxMDIuODA0IDkxLjY2NyAxMDIuOTc5QzkxLjg3NDcgMTAzLjE1NSA5Mi4xNDE0IDEwMy4yNDMgOTIuNDY3MyAxMDMuMjQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTguMzI3MSAxMDIuNTQ0TDk5LjY4MDcgOTguMTg4NUgxMDAuNzQ0TDk4LjQwNzcgMTA0Ljg5N0M5OC4wNDYxIDEwNS44NjQgOTcuNDcxNCAxMDYuMzQ3IDk2LjY4MzYgMTA2LjM0N0w5Ni40OTU2IDEwNi4zMzFMOTYuMTI1IDEwNi4yNjFWMTA1LjQ1Nkw5Ni4zOTM2IDEwNS40NzdDOTYuNzMwMSAxMDUuNDc3IDk2Ljk5MTUgMTA1LjQwOSA5Ny4xNzc3IDEwNS4yNzNDOTcuMzY3NSAxMDUuMTM3IDk3LjUyMzMgMTA0Ljg4OCA5Ny42NDUgMTA0LjUyNkw5Ny44NjUyIDEwMy45MzZMOTUuNzkyIDk4LjE4ODVIOTYuODc3TDk4LjMyNzEgMTAyLjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjkyMzMgMTQwLjM0MkM2NS45MjMzIDE0MS4xMDkgNjUuNzk0NCAxNDEuNzc4IDY1LjUzNjYgMTQyLjM1MUM2NS4yNzg4IDE0Mi45MiA2NC45MTM2IDE0My4zNTUgNjQuNDQwOSAxNDMuNjU2QzYzLjk2ODMgMTQzLjk1NyA2My40MTY4IDE0NC4xMDcgNjIuNzg2NiAxNDQuMTA3QzYyLjE3MDcgMTQ0LjEwNyA2MS42MjQ3IDE0My45NTcgNjEuMTQ4NCAxNDMuNjU2QzYwLjY3MjIgMTQzLjM1MiA2MC4zMDE2IDE0Mi45MiA2MC4wMzY2IDE0Mi4zNjJDNTkuNzc1MiAxNDEuOCA1OS42NDEgMTQxLjE1IDU5LjYzMzggMTQwLjQxMlYxMzkuODQ4QzU5LjYzMzggMTM5LjA5NiA1OS43NjQ1IDEzOC40MzIgNjAuMDI1OSAxMzcuODU1QzYwLjI4NzMgMTM3LjI3OSA2MC42NTYxIDEzNi44MzkgNjEuMTMyMyAxMzYuNTM0QzYxLjYxMjEgMTM2LjIyNiA2Mi4xNiAxMzYuMDcyIDYyLjc3NTkgMTM2LjA3MkM2My40MDI1IDEzNi4wNzIgNjMuOTUzOSAxMzYuMjI0IDY0LjQzMDIgMTM2LjUyOUM2NC45MSAxMzYuODMgNjUuMjc4OCAxMzcuMjY4IDY1LjUzNjYgMTM3Ljg0NUM2NS43OTQ0IDEzOC40MTggNjUuOTIzMyAxMzkuMDg1IDY1LjkyMzMgMTM5Ljg0OFYxNDAuMzQyWk02NC44OTc1IDEzOS44MzdDNjQuODk3NSAxMzguOTEgNjQuNzExMyAxMzguMTk5IDY0LjMzODkgMTM3LjcwNUM2My45NjY1IDEzNy4yMDcgNjMuNDQ1NSAxMzYuOTU4IDYyLjc3NTkgMTM2Ljk1OEM2Mi4xMjQyIDEzNi45NTggNjEuNjEwNCAxMzcuMjA3IDYxLjIzNDQgMTM3LjcwNUM2MC44NjIgMTM4LjE5OSA2MC42NzA0IDEzOC44ODcgNjAuNjU5NyAxMzkuNzY4VjE0MC4zNDJDNjAuNjU5NyAxNDEuMjQxIDYwLjg0NzcgMTQxLjk0OCA2MS4yMjM2IDE0Mi40NjRDNjEuNjAzMiAxNDIuOTc2IDYyLjEyNDIgMTQzLjIzMiA2Mi43ODY2IDE0My4yMzJDNjMuNDUyNiAxNDMuMjMyIDYzLjk2ODMgMTQyLjk5IDY0LjMzMzUgMTQyLjUwN0M2NC42OTg3IDE0Mi4wMiA2NC44ODY3IDE0MS4zMjMgNjQuODk3NSAxNDAuNDE3VjEzOS44MzdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDEzOC4xODhMNjguMjg2NiAxMzguOTE5QzY4LjczMDYgMTM4LjM2IDY5LjMxMDcgMTM4LjA4MSA3MC4wMjY5IDEzOC4wODFDNzEuMjU1IDEzOC4wODEgNzEuODc0NSAxMzguNzc0IDcxLjg4NTMgMTQwLjE2VjE0NEg3MC44OTE2VjE0MC4xNTRDNzAuODg4IDEzOS43MzUgNzAuNzkxMyAxMzkuNDI2IDcwLjYwMTYgMTM5LjIyNUM3MC40MTU0IDEzOS4wMjUgNzAuMTIzNSAxMzguOTI0IDY5LjcyNjEgMTM4LjkyNEM2OS40MDM4IDEzOC45MjQgNjkuMTIwOSAxMzkuMDEgNjguODc3NCAxMzkuMTgyQzY4LjYzNCAxMzkuMzU0IDY4LjQ0NDIgMTM5LjU4IDY4LjMwODEgMTM5Ljg1OVYxNDRINjcuMzE0NVYxMzguMTg4SDY4LjI1NDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NS43OTU0IDE0NC4xMDdDNzUuMDA3NiAxNDQuMTA3IDc0LjM2NjcgMTQzLjg1IDczLjg3MjYgMTQzLjMzNEM3My4zNzg0IDE0Mi44MTUgNzMuMTMxMyAxNDIuMTIyIDczLjEzMTMgMTQxLjI1NVYxNDEuMDczQzczLjEzMTMgMTQwLjQ5NiA3My4yNDA2IDEzOS45ODIgNzMuNDU5IDEzOS41MzFDNzMuNjgxIDEzOS4wNzYgNzMuOTg4OSAxMzguNzIyIDc0LjM4MjggMTM4LjQ2OEM3NC43ODAzIDEzOC4yMSA3NS4yMSAxMzguMDgxIDc1LjY3MTkgMTM4LjA4MUM3Ni40Mjc0IDEzOC4wODEgNzcuMDE0NiAxMzguMzMgNzcuNDMzNiAxMzguODI4Qzc3Ljg1MjUgMTM5LjMyNSA3OC4wNjIgMTQwLjAzOCA3OC4wNjIgMTQwLjk2NVYxNDEuMzc5SDc0LjEyNUM3NC4xMzkzIDE0MS45NTIgNzQuMzA1OCAxNDIuNDE2IDc0LjYyNDUgMTQyLjc3Qzc0Ljk0NjggMTQzLjEyMSA3NS4zNTUgMTQzLjI5NiA3NS44NDkxIDE0My4yOTZDNzYuMiAxNDMuMjk2IDc2LjQ5NzIgMTQzLjIyNSA3Ni43NDA3IDE0My4wODJDNzYuOTg0MiAxNDIuOTM4IDc3LjE5NzMgMTQyLjc0OSA3Ny4zNzk5IDE0Mi41MTJMNzcuOTg2OCAxNDIuOTg1Qzc3LjQ5OTggMTQzLjczMyA3Ni43Njk0IDE0NC4xMDcgNzUuNzk1NCAxNDQuMTA3Wk03NS42NzE5IDEzOC44OTdDNzUuMjcwOCAxMzguODk3IDc0LjkzNDIgMTM5LjA0NCA3NC42NjIxIDEzOS4zMzhDNzQuMzkgMTM5LjYyOCA3NC4yMjE3IDE0MC4wMzYgNzQuMTU3MiAxNDAuNTYySDc3LjA2ODRWMTQwLjQ4N0M3Ny4wMzk3IDEzOS45ODIgNzYuOTAzNiAxMzkuNTkyIDc2LjY2MDIgMTM5LjMxNkM3Ni40MTY3IDEzOS4wMzcgNzYuMDg3MiAxMzguODk3IDc1LjY3MTkgMTM4Ljg5N1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTgxLjI4NDcgMTQxLjA4M0g3OC42NjM2VjE0MC4yNzJIODEuMjg0N1YxNDEuMDgzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNODcuNDAyMyAxNDIuNjNMODguNTE5NSAxMzguMTg4SDg5LjUxMzJMODcuODIxMyAxNDRIODcuMDE1Nkw4NS42MDMgMTM5LjU5Nkw4NC4yMjggMTQ0SDgzLjQyMjRMODEuNzM1OCAxMzguMTg4SDgyLjcyNDFMODMuODY4MiAxNDIuNTM5TDg1LjIyMTcgMTM4LjE4OEg4Ni4wMjJMODcuNDAyMyAxNDIuNjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NC4xMTYyIDE0NEM5NC4wNTg5IDE0My44ODUgOTQuMDEyNCAxNDMuNjgxIDkzLjk3NjYgMTQzLjM4OEM5My41MTQ2IDE0My44NjggOTIuOTYzMiAxNDQuMTA3IDkyLjMyMjMgMTQ0LjEwN0M5MS43NDkzIDE0NC4xMDcgOTEuMjc4NSAxNDMuOTQ2IDkwLjkwOTcgMTQzLjYyNEM5MC41NDQ0IDE0My4yOTggOTAuMzYxOCAxNDIuODg2IDkwLjM2MTggMTQyLjM4OUM5MC4zNjE4IDE0MS43ODQgOTAuNTkxIDE0MS4zMTQgOTEuMDQ5MyAxNDAuOTgxQzkxLjUxMTIgMTQwLjY0NSA5Mi4xNTkzIDE0MC40NzcgOTIuOTkzNyAxNDAuNDc3SDkzLjk2MDRWMTQwLjAyQzkzLjk2MDQgMTM5LjY3MyA5My44NTY2IDEzOS4zOTcgOTMuNjQ4OSAxMzkuMTkzQzkzLjQ0MTIgMTM4Ljk4NSA5My4xMzUxIDEzOC44ODEgOTIuNzMwNSAxMzguODgxQzkyLjM3NiAxMzguODgxIDkyLjA3ODggMTM4Ljk3MSA5MS44Mzg5IDEzOS4xNUM5MS41OTkgMTM5LjMyOSA5MS40NzkgMTM5LjU0NiA5MS40NzkgMTM5LjhIOTAuNDhDOTAuNDggMTM5LjUxIDkwLjU4MiAxMzkuMjMgOTAuNzg2MSAxMzguOTYyQzkwLjk5MzggMTM4LjY5IDkxLjI3MzEgMTM4LjQ3NSA5MS42MjQgMTM4LjMxN0M5MS45Nzg1IDEzOC4xNiA5Mi4zNjcgMTM4LjA4MSA5Mi43ODk2IDEzOC4wODFDOTMuNDU5MSAxMzguMDgxIDkzLjk4MzcgMTM4LjI0OSA5NC4zNjMzIDEzOC41ODZDOTQuNzQyOCAxMzguOTE5IDk0LjkzOTggMTM5LjM3OSA5NC45NTQxIDEzOS45NjZWMTQyLjY0MUM5NC45NTQxIDE0My4xNzUgOTUuMDIyMSAxNDMuNTk5IDk1LjE1ODIgMTQzLjkxNFYxNDRIOTQuMTE2MlpNOTIuNDY3MyAxNDMuMjQzQzkyLjc3ODggMTQzLjI0MyA5My4wNzQyIDE0My4xNjIgOTMuMzUzNSAxNDMuMDAxQzkzLjYzMjggMTQyLjg0IDkzLjgzNTEgMTQyLjYzIDkzLjk2MDQgMTQyLjM3M1YxNDEuMThIOTMuMTgxNkM5MS45NjQyIDE0MS4xOCA5MS4zNTU1IDE0MS41MzYgOTEuMzU1NSAxNDIuMjQ5QzkxLjM1NTUgMTQyLjU2MSA5MS40NTkzIDE0Mi44MDQgOTEuNjY3IDE0Mi45NzlDOTEuODc0NyAxNDMuMTU1IDkyLjE0MTQgMTQzLjI0MyA5Mi40NjczIDE0My4yNDNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05OC4zMjcxIDE0Mi41NDRMOTkuNjgwNyAxMzguMTg4SDEwMC43NDRMOTguNDA3NyAxNDQuODk3Qzk4LjA0NjEgMTQ1Ljg2NCA5Ny40NzE0IDE0Ni4zNDcgOTYuNjgzNiAxNDYuMzQ3TDk2LjQ5NTYgMTQ2LjMzMUw5Ni4xMjUgMTQ2LjI2MVYxNDUuNDU2TDk2LjM5MzYgMTQ1LjQ3N0M5Ni43MzAxIDE0NS40NzcgOTYuOTkxNSAxNDUuNDA5IDk3LjE3NzcgMTQ1LjI3M0M5Ny4zNjc1IDE0NS4xMzcgOTcuNTIzMyAxNDQuODg4IDk3LjY0NSAxNDQuNTI2TDk3Ljg2NTIgMTQzLjkzNkw5NS43OTIgMTM4LjE4OEg5Ni44NzdMOTguMzI3MSAxNDIuNTQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCAxMDMuNDc5QzEyLjc3MzQgMTAzLjMwNyAxMi44MjM2IDEwMy4xNjQgMTIuOTIzOCAxMDMuMDQ5QzEzLjAyNzcgMTAyLjkzNSAxMy4xODE2IDEwMi44NzcgMTMuMzg1NyAxMDIuODc3QzEzLjU4OTggMTAyLjg3NyAxMy43NDM4IDEwMi45MzUgMTMuODQ3NyAxMDMuMDQ5QzEzLjk1NTEgMTAzLjE2NCAxNC4wMDg4IDEwMy4zMDcgMTQuMDA4OCAxMDMuNDc5QzE0LjAwODggMTAzLjY0NCAxMy45NTUxIDEwMy43ODIgMTMuODQ3NyAxMDMuODkzQzEzLjc0MzggMTA0LjAwNCAxMy41ODk4IDEwNC4wNTkgMTMuMzg1NyAxMDQuMDU5QzEzLjE4MTYgMTA0LjA1OSAxMy4wMjc3IDEwNC4wMDQgMTIuOTIzOCAxMDMuODkzQzEyLjgyMzYgMTAzLjc4MiAxMi43NzM0IDEwMy42NDQgMTIuNzczNCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCAxMDMuNDc5QzE1LjY3MzggMTAzLjMwNyAxNS43MjQgMTAzLjE2NCAxNS44MjQyIDEwMy4wNDlDMTUuOTI4MSAxMDIuOTM1IDE2LjA4MiAxMDIuODc3IDE2LjI4NjEgMTAyLjg3N0MxNi40OTAyIDEwMi44NzcgMTYuNjQ0MiAxMDIuOTM1IDE2Ljc0OCAxMDMuMDQ5QzE2Ljg1NTUgMTAzLjE2NCAxNi45MDkyIDEwMy4zMDcgMTYuOTA5MiAxMDMuNDc5QzE2LjkwOTIgMTAzLjY0NCAxNi44NTU1IDEwMy43ODIgMTYuNzQ4IDEwMy44OTNDMTYuNjQ0MiAxMDQuMDA0IDE2LjQ5MDIgMTA0LjA1OSAxNi4yODYxIDEwNC4wNTlDMTYuMDgyIDEwNC4wNTkgMTUuOTI4MSAxMDQuMDA0IDE1LjgyNDIgMTAzLjg5M0MxNS43MjQgMTAzLjc4MiAxNS42NzM4IDEwMy42NDQgMTUuNjczOCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTguMjg5NiAxMDEuMDQxQzE4LjI4OTYgMTAwLjQ3MSAxOC40MDA2IDk5Ljk1OTEgMTguNjIyNiA5OS41MDQ0QzE4Ljg0ODEgOTkuMDQ5NiAxOS4xNTk3IDk4LjY5ODcgMTkuNTU3MSA5OC40NTE3QzE5Ljk1ODIgOTguMjA0NiAyMC40MTQ3IDk4LjA4MTEgMjAuOTI2OCA5OC4wODExQzIxLjcxODEgOTguMDgxMSAyMi4zNTczIDk4LjM1NSAyMi44NDQyIDk4LjkwMjhDMjMuMzM0OCA5OS40NTA3IDIzLjU4MDEgMTAwLjE3OSAyMy41ODAxIDEwMS4wODlWMTAxLjE1OUMyMy41ODAxIDEwMS43MjQgMjMuNDcwOSAxMDIuMjMzIDIzLjI1MjQgMTAyLjY4NEMyMy4wMzc2IDEwMy4xMzIgMjIuNzI3OSAxMDMuNDgxIDIyLjMyMzIgMTAzLjczMUMyMS45MjIyIDEwMy45ODIgMjEuNDYwMyAxMDQuMTA3IDIwLjkzNzUgMTA0LjEwN0MyMC4xNDk3IDEwNC4xMDcgMTkuNTEwNiAxMDMuODMzIDE5LjAyIDEwMy4yODZDMTguNTMzIDEwMi43MzggMTguMjg5NiAxMDIuMDEzIDE4LjI4OTYgMTAxLjExVjEwMS4wNDFaTTE5LjI4ODYgMTAxLjE1OUMxOS4yODg2IDEwMS44MDMgMTkuNDM3MiAxMDIuMzIxIDE5LjczNDQgMTAyLjcxMUMyMC4wMzUyIDEwMy4xMDEgMjAuNDM2MiAxMDMuMjk2IDIwLjkzNzUgMTAzLjI5NkMyMS40NDI0IDEwMy4yOTYgMjEuODQzNCAxMDMuMDk5IDIyLjE0MDYgMTAyLjcwNkMyMi40Mzc4IDEwMi4zMDggMjIuNTg2NCAxMDEuNzUzIDIyLjU4NjQgMTAxLjA0MUMyMi41ODY0IDEwMC40MDMgMjIuNDM0MiA5OS44ODc1IDIyLjEyOTkgOTkuNDkzN0MyMS44MjkxIDk5LjA5NjIgMjEuNDI4MSA5OC44OTc1IDIwLjkyNjggOTguODk3NUMyMC40MzYyIDk4Ljg5NzUgMjAuMDQwNSA5OS4wOTI2IDE5LjczOTcgOTkuNDgyOUMxOS40MzkgOTkuODczMiAxOS4yODg2IDEwMC40MzIgMTkuMjg4NiAxMDEuMTU5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjYuMTY4OSA5OS42MDY0SDI2LjkxNTVDMjcuMzg0NiA5OS41OTkzIDI3Ljc1MzQgOTkuNDc1NyAyOC4wMjIgOTkuMjM1OEMyOC4yOTA1IDk4Ljk5NTkgMjguNDI0OCA5OC42NzE5IDI4LjQyNDggOTguMjYzN0MyOC40MjQ4IDk3LjM0NyAyNy45NjgzIDk2Ljg4ODcgMjcuMDU1MiA5Ni44ODg3QzI2LjYyNTUgOTYuODg4NyAyNi4yODE3IDk3LjAxMjIgMjYuMDIzOSA5Ny4yNTkzQzI1Ljc2OTcgOTcuNTAyOCAyNS42NDI2IDk3LjgyNjggMjUuNjQyNiA5OC4yMzE0SDI0LjY0ODlDMjQuNjQ4OSA5Ny42MTIgMjQuODc0NSA5Ny4wOTgxIDI1LjMyNTcgOTYuNjg5OUMyNS43ODA0IDk2LjI3ODIgMjYuMzU2OSA5Ni4wNzIzIDI3LjA1NTIgOTYuMDcyM0MyNy43OTI4IDk2LjA3MjMgMjguMzcxMSA5Ni4yNjc0IDI4Ljc5IDk2LjY1NzdDMjkuMjA5IDk3LjA0OCAyOS40MTg1IDk3LjU5MDUgMjkuNDE4NSA5OC4yODUyQzI5LjQxODUgOTguNjI1MyAyOS4zMDc1IDk4Ljk1NDggMjkuMDg1NCA5OS4yNzM0QzI4Ljg2NyA5OS41OTIxIDI4LjU2OCA5OS44MzAyIDI4LjE4ODUgOTkuOTg3OEMyOC42MTgyIDEwMC4xMjQgMjguOTQ5NCAxMDAuMzQ5IDI5LjE4MjEgMTAwLjY2NUMyOS40MTg1IDEwMC45OCAyOS41MzY2IDEwMS4zNjUgMjkuNTM2NiAxMDEuODE5QzI5LjUzNjYgMTAyLjUyMSAyOS4zMDc1IDEwMy4wNzggMjguODQ5MSAxMDMuNDlDMjguMzkwOCAxMDMuOTAyIDI3Ljc5NDYgMTA0LjEwNyAyNy4wNjA1IDEwNC4xMDdDMjYuMzI2NSAxMDQuMTA3IDI1LjcyODUgMTAzLjkwOSAyNS4yNjY2IDEwMy41MTFDMjQuODA4MyAxMDMuMTE0IDI0LjU3OTEgMTAyLjU4OSAyNC41NzkxIDEwMS45MzhIMjUuNTc4MUMyNS41NzgxIDEwMi4zNDkgMjUuNzEyNCAxMDIuNjc5IDI1Ljk4MSAxMDIuOTI2QzI2LjI0OTUgMTAzLjE3MyAyNi42MDk0IDEwMy4yOTYgMjcuMDYwNSAxMDMuMjk2QzI3LjU0MDQgMTAzLjI5NiAyNy45MDc0IDEwMy4xNzEgMjguMTYxNiAxMDIuOTJDMjguNDE1OSAxMDIuNjcgMjguNTQzIDEwMi4zMSAyOC41NDMgMTAxLjg0MUMyOC41NDMgMTAxLjM4NiAyOC40MDMzIDEwMS4wMzcgMjguMTI0IDEwMC43OTNDMjcuODQ0NyAxMDAuNTUgMjcuNDQxOSAxMDAuNDI1IDI2LjkxNTUgMTAwLjQxN0gyNi4xNjg5Vjk5LjYwNjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zMC43NjEyIDEwMS4wNDZDMzAuNzYxMiAxMDAuMTU0IDMwLjk3MjUgOTkuNDM4MiAzMS4zOTUgOTguODk3NUMzMS44MTc1IDk4LjM1MzIgMzIuMzcwOCA5OC4wODExIDMzLjA1NDcgOTguMDgxMUMzMy43MzUgOTguMDgxMSAzNC4yNzM5IDk4LjMxMzggMzQuNjcxNCA5OC43NzkzVjk1Ljc1SDM1LjY2NVYxMDRIMzQuNzUyTDM0LjcwMzYgMTAzLjM3N0MzNC4zMDYyIDEwMy44NjQgMzMuNzUyOSAxMDQuMTA3IDMzLjA0MzkgMTA0LjEwN0MzMi4zNzA4IDEwNC4xMDcgMzEuODIxMSAxMDMuODMyIDMxLjM5NSAxMDMuMjhDMzAuOTcyNSAxMDIuNzI5IDMwLjc2MTIgMTAyLjAwOSAzMC43NjEyIDEwMS4xMjFWMTAxLjA0NlpNMzEuNzU0OSAxMDEuMTU5QzMxLjc1NDkgMTAxLjgxOCAzMS44OTEgMTAyLjMzMyAzMi4xNjMxIDEwMi43MDZDMzIuNDM1MiAxMDMuMDc4IDMyLjgxMTIgMTAzLjI2NCAzMy4yOTEgMTAzLjI2NEMzMy45MjEyIDEwMy4yNjQgMzQuMzgxMyAxMDIuOTgxIDM0LjY3MTQgMTAyLjQxNlY5OS43NDYxQzM0LjM3NDIgOTkuMTk4MiAzMy45MTc2IDk4LjkyNDMgMzMuMzAxOCA5OC45MjQzQzMyLjgxNDggOTguOTI0MyAzMi40MzUyIDk5LjExMjMgMzIuMTYzMSA5OS40ODgzQzMxLjg5MSA5OS44NjQzIDMxLjc1NDkgMTAwLjQyMSAzMS43NTQ5IDEwMS4xNTlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00MC4wMjEgOTkuMDgwMUMzOS44NzA2IDk5LjA1NSAzOS43MDc3IDk5LjA0MjUgMzkuNTMyMiA5OS4wNDI1QzM4Ljg4MDUgOTkuMDQyNSAzOC40MzgzIDk5LjMyIDM4LjIwNTYgOTkuODc1VjEwNEgzNy4yMTE5Vjk4LjE4ODVIMzguMTc4N0wzOC4xOTQ4IDk4Ljg1OTlDMzguNTIwNyA5OC4zNDA3IDM4Ljk4MjYgOTguMDgxMSAzOS41ODA2IDk4LjA4MTFDMzkuNzczOSA5OC4wODExIDM5LjkyMDcgOTguMTA2MSA0MC4wMjEgOTguMTU2MlY5OS4wODAxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuODc5NCA5OC4xODg1TDQxLjkxMTYgOTguOTE4OUM0Mi4zNTU2IDk4LjM2MDQgNDIuOTM1NyA5OC4wODExIDQzLjY1MTkgOTguMDgxMUM0NC44OCA5OC4wODExIDQ1LjQ5OTUgOTguNzczOSA0NS41MTAzIDEwMC4xNlYxMDRINDQuNTE2NlYxMDAuMTU0QzQ0LjUxMyA5OS43MzU0IDQ0LjQxNjMgOTkuNDI1NiA0NC4yMjY2IDk5LjIyNTFDNDQuMDQwNCA5OS4wMjQ2IDQzLjc0ODUgOTguOTI0MyA0My4zNTExIDk4LjkyNDNDNDMuMDI4OCA5OC45MjQzIDQyLjc0NTkgOTkuMDEwMyA0Mi41MDI0IDk5LjE4MjFDNDIuMjU5IDk5LjM1NCA0Mi4wNjkyIDk5LjU3OTYgNDEuOTMzMSA5OS44NTg5VjEwNEg0MC45Mzk1Vjk4LjE4ODVINDEuODc5NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTEyLjc3MzQgMTQzLjQ3OUMxMi43NzM0IDE0My4zMDcgMTIuODIzNiAxNDMuMTY0IDEyLjkyMzggMTQzLjA0OUMxMy4wMjc3IDE0Mi45MzUgMTMuMTgxNiAxNDIuODc3IDEzLjM4NTcgMTQyLjg3N0MxMy41ODk4IDE0Mi44NzcgMTMuNzQzOCAxNDIuOTM1IDEzLjg0NzcgMTQzLjA0OUMxMy45NTUxIDE0My4xNjQgMTQuMDA4OCAxNDMuMzA3IDE0LjAwODggMTQzLjQ3OUMxNC4wMDg4IDE0My42NDQgMTMuOTU1MSAxNDMuNzgyIDEzLjg0NzcgMTQzLjg5M0MxMy43NDM4IDE0NC4wMDQgMTMuNTg5OCAxNDQuMDU5IDEzLjM4NTcgMTQ0LjA1OUMxMy4xODE2IDE0NC4wNTkgMTMuMDI3NyAxNDQuMDA0IDEyLjkyMzggMTQzLjg5M0MxMi44MjM2IDE0My43ODIgMTIuNzczNCAxNDMuNjQ0IDEyLjc3MzQgMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTE1LjY3MzggMTQzLjQ3OUMxNS42NzM4IDE0My4zMDcgMTUuNzI0IDE0My4xNjQgMTUuODI0MiAxNDMuMDQ5QzE1LjkyODEgMTQyLjkzNSAxNi4wODIgMTQyLjg3NyAxNi4yODYxIDE0Mi44NzdDMTYuNDkwMiAxNDIuODc3IDE2LjY0NDIgMTQyLjkzNSAxNi43NDggMTQzLjA0OUMxNi44NTU1IDE0My4xNjQgMTYuOTA5MiAxNDMuMzA3IDE2LjkwOTIgMTQzLjQ3OUMxNi45MDkyIDE0My42NDQgMTYuODU1NSAxNDMuNzgyIDE2Ljc0OCAxNDMuODkzQzE2LjY0NDIgMTQ0LjAwNCAxNi40OTAyIDE0NC4wNTkgMTYuMjg2MSAxNDQuMDU5QzE2LjA4MiAxNDQuMDU5IDE1LjkyODEgMTQ0LjAwNCAxNS44MjQyIDE0My44OTNDMTUuNzI0IDE0My43ODIgMTUuNjczOCAxNDMuNjQ0IDE1LjY3MzggMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTIxLjM2MTggMTM5LjA4QzIxLjIxMTQgMTM5LjA1NSAyMS4wNDg1IDEzOS4wNDIgMjAuODczIDEzOS4wNDJDMjAuMjIxNCAxMzkuMDQyIDE5Ljc3OTEgMTM5LjMyIDE5LjU0NjQgMTM5Ljg3NVYxNDRIMTguNTUyN1YxMzguMTg4SDE5LjUxOTVMMTkuNTM1NiAxMzguODZDMTkuODYxNSAxMzguMzQxIDIwLjMyMzQgMTM4LjA4MSAyMC45MjE0IDEzOC4wODFDMjEuMTE0NyAxMzguMDgxIDIxLjI2MTYgMTM4LjEwNiAyMS4zNjE4IDEzOC4xNTZWMTM5LjA4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjQuMjk0NCAxNDIuNjUyTDI1LjczMzkgMTM4LjE4OEgyNi43NDlMMjQuNjY1IDE0NEgyMy45MDc3TDIxLjgwMjIgMTM4LjE4OEgyMi44MTc0TDI0LjI5NDQgMTQyLjY1MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMxLjQxMTEgMTQwLjU2MkMzMS4yMDM1IDE0MC44MSAzMC45NTQ2IDE0MS4wMDggMzAuNjY0NiAxNDEuMTU5QzMwLjM3ODEgMTQxLjMwOSAzMC4wNjMgMTQxLjM4NCAyOS43MTkyIDE0MS4zODRDMjkuMjY4MSAxNDEuMzg0IDI4Ljg3NDIgMTQxLjI3MyAyOC41Mzc2IDE0MS4wNTFDMjguMjA0NiAxNDAuODI5IDI3Ljk0NjggMTQwLjUxOCAyNy43NjQyIDE0MC4xMTdDMjcuNTgxNSAxMzkuNzEyIDI3LjQ5MDIgMTM5LjI2NiAyNy40OTAyIDEzOC43NzlDMjcuNDkwMiAxMzguMjU3IDI3LjU4ODcgMTM3Ljc4NiAyNy43ODU2IDEzNy4zNjdDMjcuOTg2MiAxMzYuOTQ4IDI4LjI2OSAxMzYuNjI3IDI4LjYzNDMgMTM2LjQwNUMyOC45OTk1IDEzNi4xODMgMjkuNDI1NiAxMzYuMDcyIDI5LjkxMjYgMTM2LjA3MkMzMC42ODYgMTM2LjA3MiAzMS4yOTQ4IDEzNi4zNjIgMzEuNzM4OCAxMzYuOTQyQzMyLjE4NjQgMTM3LjUxOSAzMi40MTAyIDEzOC4zMDcgMzIuNDEwMiAxMzkuMzA2VjEzOS41OTZDMzIuNDEwMiAxNDEuMTE4IDMyLjEwOTQgMTQyLjIyOSAzMS41MDc4IDE0Mi45MzFDMzAuOTA2MiAxNDMuNjI5IDI5Ljk5ODUgMTQzLjk4NyAyOC43ODQ3IDE0NC4wMDVIMjguNTkxM1YxNDMuMTY3SDI4LjgwMDhDMjkuNjIwOCAxNDMuMTUzIDMwLjI1MSAxNDIuOTQgMzAuNjkxNCAxNDIuNTI4QzMxLjEzMTggMTQyLjExMyAzMS4zNzE3IDE0MS40NTggMzEuNDExMSAxNDAuNTYyWk0yOS44ODA0IDE0MC41NjJDMzAuMjEzNCAxNDAuNTYyIDMwLjUxOTUgMTQwLjQ2IDMwLjc5ODggMTQwLjI1NkMzMS4wODE3IDE0MC4wNTIgMzEuMjg3NiAxMzkuOCAzMS40MTY1IDEzOS40OTlWMTM5LjEwMkMzMS40MTY1IDEzOC40NSAzMS4yNzUxIDEzNy45MiAzMC45OTIyIDEzNy41MTJDMzAuNzA5MyAxMzcuMTA0IDMwLjM1MTIgMTM2Ljg5OSAyOS45MTggMTM2Ljg5OUMyOS40ODExIDEzNi44OTkgMjkuMTMwMiAxMzcuMDY4IDI4Ljg2NTIgMTM3LjQwNEMyOC42MDAzIDEzNy43MzcgMjguNDY3OCAxMzguMTc4IDI4LjQ2NzggMTM4LjcyNkMyOC40Njc4IDEzOS4yNTkgMjguNTk0OSAxMzkuNyAyOC44NDkxIDE0MC4wNDdDMjkuMTA2OSAxNDAuMzkxIDI5LjQ1MDcgMTQwLjU2MiAyOS44ODA0IDE0MC41NjJaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNS41MDM5IDE0MS4zMDlMMzQuODgwOSAxNDEuOTU5VjE0NEgzMy44ODcyVjEzNS43NUgzNC44ODA5VjE0MC43NEwzNS40MTI2IDE0MC4xMDFMMzcuMjIyNyAxMzguMTg4SDM4LjQzMTJMMzYuMTY5OSAxNDAuNjE2TDM4LjY5NDMgMTQ0SDM3LjUyODhMMzUuNTAzOSAxNDEuMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuNzYxMiAxNDQuMTA3QzQwLjk3MzUgMTQ0LjEwNyA0MC4zMzI1IDE0My44NSAzOS44Mzg0IDE0My4zMzRDMzkuMzQ0MiAxNDIuODE1IDM5LjA5NzIgMTQyLjEyMiAzOS4wOTcyIDE0MS4yNTVWMTQxLjA3M0MzOS4wOTcyIDE0MC40OTYgMzkuMjA2NCAxMzkuOTgyIDM5LjQyNDggMTM5LjUzMUMzOS42NDY4IDEzOS4wNzYgMzkuOTU0OCAxMzguNzIyIDQwLjM0ODYgMTM4LjQ2OEM0MC43NDYxIDEzOC4yMSA0MS4xNzU4IDEzOC4wODEgNDEuNjM3NyAxMzguMDgxQzQyLjM5MzIgMTM4LjA4MSA0Mi45ODA1IDEzOC4zMyA0My4zOTk0IDEzOC44MjhDNDMuODE4NCAxMzkuMzI1IDQ0LjAyNzggMTQwLjAzOCA0NC4wMjc4IDE0MC45NjVWMTQxLjM3OUg0MC4wOTA4QzQwLjEwNTEgMTQxLjk1MiA0MC4yNzE2IDE0Mi40MTYgNDAuNTkwMyAxNDIuNzdDNDAuOTEyNiAxNDMuMTIxIDQxLjMyMDggMTQzLjI5NiA0MS44MTQ5IDE0My4yOTZDNDIuMTY1OSAxNDMuMjk2IDQyLjQ2MzEgMTQzLjIyNSA0Mi43MDY1IDE0My4wODJDNDIuOTUgMTQyLjkzOCA0My4xNjMxIDE0Mi43NDkgNDMuMzQ1NyAxNDIuNTEyTDQzLjk1MjYgMTQyLjk4NUM0My40NjU3IDE0My43MzMgNDIuNzM1MiAxNDQuMTA3IDQxLjc2MTIgMTQ0LjEwN1pNNDEuNjM3NyAxMzguODk3QzQxLjIzNjcgMTM4Ljg5NyA0MC45MDAxIDEzOS4wNDQgNDAuNjI3OSAxMzkuMzM4QzQwLjM1NTggMTM5LjYyOCA0MC4xODc1IDE0MC4wMzYgNDAuMTIzIDE0MC41NjJINDMuMDM0MlYxNDAuNDg3QzQzLjAwNTUgMTM5Ljk4MiA0Mi44Njk1IDEzOS41OTIgNDIuNjI2IDEzOS4zMTZDNDIuMzgyNSAxMzkuMDM3IDQyLjA1MzEgMTM4Ljg5NyA0MS42Mzc3IDEzOC44OTdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNDYuNDQ1IDU3LjI3NTRIMTQ0LjAwN1Y2NEgxNDIuNjU5VjU3LjI3NTRIMTQwLjI0MlY1Ni4xNzk3SDE0Ni40NDVWNTcuMjc1NFoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0OC43MzkgNjRIMTQ3LjQzNFY1OC4xODg1SDE0OC43MzlWNjRaTTE0Ny4zNTMgNTYuNjc5MkMxNDcuMzUzIDU2LjQ3ODcgMTQ3LjQxNiA1Ni4zMTIyIDE0Ny41NDEgNTYuMTc5N0MxNDcuNjcgNTYuMDQ3MiAxNDcuODUzIDU1Ljk4MSAxNDguMDg5IDU1Ljk4MUMxNDguMzI1IDU1Ljk4MSAxNDguNTA4IDU2LjA0NzIgMTQ4LjYzNyA1Ni4xNzk3QzE0OC43NjYgNTYuMzEyMiAxNDguODMgNTYuNDc4NyAxNDguODMgNTYuNjc5MkMxNDguODMgNTYuODc2MSAxNDguNzY2IDU3LjA0MDkgMTQ4LjYzNyA1Ny4xNzMzQzE0OC41MDggNTcuMzAyMiAxNDguMzI1IDU3LjM2NjcgMTQ4LjA4OSA1Ny4zNjY3QzE0Ny44NTMgNTcuMzY2NyAxNDcuNjcgNTcuMzAyMiAxNDcuNTQxIDU3LjE3MzNDMTQ3LjQxNiA1Ny4wNDA5IDE0Ny4zNTMgNTYuODc2MSAxNDcuMzUzIDU2LjY3OTJaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNTEuMzkyIDU4LjE4ODVMMTUxLjQzIDU4Ljc5NTRDMTUxLjgzOCA1OC4zMTkyIDE1Mi4zOTYgNTguMDgxMSAxNTMuMTA1IDU4LjA4MTFDMTUzLjg4MiA1OC4wODExIDE1NC40MTQgNTguMzc4MyAxNTQuNzAxIDU4Ljk3MjdDMTU1LjEyMyA1OC4zNzgzIDE1NS43MTggNTguMDgxMSAxNTYuNDg0IDU4LjA4MTFDMTU3LjEyNSA1OC4wODExIDE1Ny42MDEgNTguMjU4MyAxNTcuOTEzIDU4LjYxMjhDMTU4LjIyOCA1OC45NjczIDE1OC4zODkgNTkuNDkwMSAxNTguMzk2IDYwLjE4MTJWNjRIMTU3LjA5MVY2MC4yMTg4QzE1Ny4wOTEgNTkuODQ5OSAxNTcuMDEgNTkuNTc5NiAxNTYuODQ5IDU5LjQwNzdDMTU2LjY4OCA1OS4yMzU4IDE1Ni40MjEgNTkuMTQ5OSAxNTYuMDQ5IDU5LjE0OTlDMTU1Ljc1MiA1OS4xNDk5IDE1NS41MDggNTkuMjMwNSAxNTUuMzE4IDU5LjM5MTZDMTU1LjEzMiA1OS41NDkyIDE1NS4wMDEgNTkuNzU2OCAxNTQuOTI2IDYwLjAxNDZMMTU0LjkzMiA2NEgxNTMuNjI2VjYwLjE3NThDMTUzLjYwOSA1OS40OTE5IDE1My4yNTkgNTkuMTQ5OSAxNTIuNTc5IDU5LjE0OTlDMTUyLjA1NiA1OS4xNDk5IDE1MS42ODYgNTkuMzYzIDE1MS40NjcgNTkuNzg5MVY2NEgxNTAuMTYyVjU4LjE4ODVIMTUxLjM5MloiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE2Mi4yOTUgNjQuMTA3NEMxNjEuNDY4IDY0LjEwNzQgMTYwLjc5NyA2My44NDc4IDE2MC4yODEgNjMuMzI4NkMxNTkuNzY5IDYyLjgwNTggMTU5LjUxMyA2Mi4xMTEyIDE1OS41MTMgNjEuMjQ0NlY2MS4wODM1QzE1OS41MTMgNjAuNTAzNCAxNTkuNjI0IDU5Ljk4NiAxNTkuODQ2IDU5LjUzMTJDMTYwLjA3MiA1OS4wNzI5IDE2MC4zODcgNTguNzE2NiAxNjAuNzkyIDU4LjQ2MjRDMTYxLjE5NiA1OC4yMDgyIDE2MS42NDcgNTguMDgxMSAxNjIuMTQ1IDU4LjA4MTFDMTYyLjkzNiA1OC4wODExIDE2My41NDcgNTguMzMzNSAxNjMuOTc3IDU4LjgzODRDMTY0LjQxIDU5LjM0MzMgMTY0LjYyNiA2MC4wNTc2IDE2NC42MjYgNjAuOTgxNFY2MS41MDc4SDE2MC44MjlDMTYwLjg2OCA2MS45ODc2IDE2MS4wMjggNjIuMzY3MiAxNjEuMzA3IDYyLjY0NjVDMTYxLjU5IDYyLjkyNTggMTYxLjk0NCA2My4wNjU0IDE2Mi4zNzEgNjMuMDY1NEMxNjIuOTY5IDYzLjA2NTQgMTYzLjQ1NiA2Mi44MjM3IDE2My44MzIgNjIuMzQwM0wxNjQuNTM1IDYzLjAxMTdDMTY0LjMwMiA2My4zNTkgMTYzLjk5MSA2My42Mjk0IDE2My42MDEgNjMuODIyOEMxNjMuMjE0IDY0LjAxMjUgMTYyLjc3OSA2NC4xMDc0IDE2Mi4yOTUgNjQuMTA3NFpNMTYyLjE0IDU5LjEyODRDMTYxLjc4MiA1OS4xMjg0IDE2MS40OTIgNTkuMjUzNyAxNjEuMjcgNTkuNTA0NEMxNjEuMDUxIDU5Ljc1NSAxNjAuOTExIDYwLjEwNDIgMTYwLjg1MSA2MC41NTE4SDE2My4zMzdWNjAuNDU1MUMxNjMuMzA5IDYwLjAxODIgMTYzLjE5MiA1OS42ODg4IDE2Mi45ODggNTkuNDY2OEMxNjIuNzg0IDU5LjI0MTIgMTYyLjUwMSA1OS4xMjg0IDE2Mi4xNCA1OS4xMjg0WiIgZmlsbD0iI0ZGQTUwMCIvPgo8cGF0aCBkPSJNMTY1LjQgNjEuMDQwNUMxNjUuNCA2MC40NzEyIDE2NS41MTMgNTkuOTU5MSAxNjUuNzM4IDU5LjUwNDRDMTY1Ljk2NCA1OS4wNDYxIDE2Ni4yODEgNTguNjk1MSAxNjYuNjg5IDU4LjQ1MTdDMTY3LjA5NyA1OC4yMDQ2IDE2Ny41NjYgNTguMDgxMSAxNjguMDk2IDU4LjA4MTFDMTY4Ljg4IDU4LjA4MTEgMTY5LjUxNiA1OC4zMzM1IDE3MC4wMDMgNTguODM4NEMxNzAuNDkzIDU5LjM0MzMgMTcwLjc1OCA2MC4wMTI5IDE3MC43OTggNjAuODQ3MkwxNzAuODAzIDYxLjE1MzNDMTcwLjgwMyA2MS43MjYyIDE3MC42OTIgNjIuMjM4MyAxNzAuNDcgNjIuNjg5NUMxNzAuMjUyIDYzLjE0MDYgMTY5LjkzNyA2My40ODk3IDE2OS41MjUgNjMuNzM2OEMxNjkuMTE3IDYzLjk4MzkgMTY4LjY0NCA2NC4xMDc0IDE2OC4xMDcgNjQuMTA3NEMxNjcuMjg3IDY0LjEwNzQgMTY2LjYzIDYzLjgzNTMgMTY2LjEzNiA2My4yOTFDMTY1LjY0NSA2Mi43NDMyIDE2NS40IDYyLjAxNDUgMTY1LjQgNjEuMTA1VjYxLjA0MDVaTTE2Ni43MDUgNjEuMTUzM0MxNjYuNzA1IDYxLjc1MTMgMTY2LjgyOSA2Mi4yMjA0IDE2Ny4wNzYgNjIuNTYwNUMxNjcuMzIzIDYyLjg5NzEgMTY3LjY2NyA2My4wNjU0IDE2OC4xMDcgNjMuMDY1NEMxNjguNTQ3IDYzLjA2NTQgMTY4Ljg4OSA2Mi44OTM2IDE2OS4xMzMgNjIuNTQ5OEMxNjkuMzggNjIuMjA2MSAxNjkuNTAzIDYxLjcwMyAxNjkuNTAzIDYxLjA0MDVDMTY5LjUwMyA2MC40NTMzIDE2OS4zNzYgNTkuOTg3OCAxNjkuMTIyIDU5LjY0NEMxNjguODcxIDU5LjMwMDMgMTY4LjUyOSA1OS4xMjg0IDE2OC4wOTYgNTkuMTI4NEMxNjcuNjcgNTkuMTI4NCAxNjcuMzMyIDU5LjI5ODUgMTY3LjA4MSA1OS42Mzg3QzE2Ni44MyA1OS45NzUzIDE2Ni43MDUgNjAuNDgwMSAxNjYuNzA1IDYxLjE1MzNaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNzUuNDI4IDYzLjQzMDdDMTc1LjA0NSA2My44ODE4IDE3NC41IDY0LjEwNzQgMTczLjc5NSA2NC4xMDc0QzE3My4xNjUgNjQuMTA3NCAxNzIuNjg3IDYzLjkyMyAxNzIuMzYxIDYzLjU1NDJDMTcyLjAzOSA2My4xODU0IDE3MS44NzcgNjIuNjUxOSAxNzEuODc3IDYxLjk1MzZWNTguMTg4NUgxNzMuMTgzVjYxLjkzNzVDMTczLjE4MyA2Mi42NzUxIDE3My40ODkgNjMuMDQzOSAxNzQuMTAxIDYzLjA0MzlDMTc0LjczNSA2My4wNDM5IDE3NS4xNjMgNjIuODE2NiAxNzUuMzg1IDYyLjM2MThWNTguMTg4NUgxNzYuNjlWNjRIMTc1LjQ2TDE3NS40MjggNjMuNDMwN1oiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE3OS42NTUgNTYuNzc1OVY1OC4xODg1SDE4MC42ODFWNTkuMTU1M0gxNzkuNjU1VjYyLjM5OTRDMTc5LjY1NSA2Mi42MjE0IDE3OS42OTggNjIuNzgyNiAxNzkuNzg0IDYyLjg4MjhDMTc5Ljg3MyA2Mi45Nzk1IDE4MC4wMzEgNjMuMDI3OCAxODAuMjU2IDYzLjAyNzhDMTgwLjQwNyA2My4wMjc4IDE4MC41NTkgNjMuMDA5OSAxODAuNzEzIDYyLjk3NDFWNjMuOTgzOUMxODAuNDE2IDY0LjA2NjIgMTgwLjEyOSA2NC4xMDc0IDE3OS44NTQgNjQuMTA3NEMxNzguODUxIDY0LjEwNzQgMTc4LjM1IDYzLjU1NDIgMTc4LjM1IDYyLjQ0NzhWNTkuMTU1M0gxNzcuMzk0VjU4LjE4ODVIMTc4LjM1VjU2Ljc3NTlIMTc5LjY1NVoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0NS4zMDEgMTAwLjY4NkgxNDIuMTU0VjEwNEgxNDAuNzk1Vjk2LjE3OTdIMTQ1Ljc2M1Y5Ny4yNzU0SDE0Mi4xNTRWOTkuNjAxMUgxNDUuMzAxVjEwMC42ODZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTAuMDA2IDEwNEMxNDkuOTQ5IDEwMy44ODkgMTQ5Ljg5OSAxMDMuNzA4IDE0OS44NTYgMTAzLjQ1OEMxNDkuNDQxIDEwMy44OTEgMTQ4LjkzMiAxMDQuMTA3IDE0OC4zMzEgMTA0LjEwN0MxNDcuNzQ3IDEwNC4xMDcgMTQ3LjI3MSAxMDMuOTQxIDE0Ni45MDIgMTAzLjYwOEMxNDYuNTMzIDEwMy4yNzUgMTQ2LjM0OSAxMDIuODYzIDE0Ni4zNDkgMTAyLjM3M0MxNDYuMzQ5IDEwMS43NTMgMTQ2LjU3OCAxMDEuMjc5IDE0Ny4wMzYgMTAwLjk0OUMxNDcuNDk4IDEwMC42MTYgMTQ4LjE1NyAxMDAuNDUgMTQ5LjAxMyAxMDAuNDVIMTQ5LjgxM1YxMDAuMDY4QzE0OS44MTMgOTkuNzY3NiAxNDkuNzI5IDk5LjUyNzcgMTQ5LjU2MSA5OS4zNDg2QzE0OS4zOTIgOTkuMTY2IDE0OS4xMzYgOTkuMDc0NyAxNDguNzkyIDk5LjA3NDdDMTQ4LjQ5NSA5OS4wNzQ3IDE0OC4yNTIgOTkuMTQ5OSAxNDguMDYyIDk5LjMwMDNDMTQ3Ljg3MiA5OS40NDcxIDE0Ny43NzcgOTkuNjM1MSAxNDcuNzc3IDk5Ljg2NDNIMTQ2LjQ3MkMxNDYuNDcyIDk5LjU0NTYgMTQ2LjU3OCA5OS4yNDg0IDE0Ni43ODkgOTguOTcyN0MxNDcgOTguNjkzNCAxNDcuMjg3IDk4LjQ3NDkgMTQ3LjY0OCA5OC4zMTc0QzE0OC4wMTQgOTguMTU5OCAxNDguNDIgOTguMDgxMSAxNDguODY4IDk4LjA4MTFDMTQ5LjU0OCA5OC4wODExIDE1MC4wOSA5OC4yNTI5IDE1MC40OTUgOTguNTk2N0MxNTAuOSA5OC45MzY4IDE1MS4xMDcgOTkuNDE2NyAxNTEuMTE4IDEwMC4wMzZWMTAyLjY1N0MxNTEuMTE4IDEwMy4xOCAxNTEuMTkyIDEwMy41OTcgMTUxLjMzOCAxMDMuOTA5VjEwNEgxNTAuMDA2Wk0xNDguNTcyIDEwMy4wNkMxNDguODMgMTAzLjA2IDE0OS4wNzIgMTAyLjk5NyAxNDkuMjk3IDEwMi44NzJDMTQ5LjUyNyAxMDIuNzQ3IDE0OS42OTggMTAyLjU3OCAxNDkuODEzIDEwMi4zNjdWMTAxLjI3MUgxNDkuMTA5QzE0OC42MjYgMTAxLjI3MSAxNDguMjYzIDEwMS4zNTYgMTQ4LjAxOSAxMDEuNTI0QzE0Ny43NzYgMTAxLjY5MiAxNDcuNjU0IDEwMS45MyAxNDcuNjU0IDEwMi4yMzhDMTQ3LjY1NCAxMDIuNDg5IDE0Ny43MzYgMTAyLjY4OSAxNDcuOTAxIDEwMi44NEMxNDguMDY5IDEwMi45ODcgMTQ4LjI5MyAxMDMuMDYgMTQ4LjU3MiAxMDMuMDZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTMuODc0IDEwNEgxNTIuNTY4Vjk4LjE4ODVIMTUzLjg3NFYxMDRaTTE1Mi40ODggOTYuNjc5MkMxNTIuNDg4IDk2LjQ3ODcgMTUyLjU1IDk2LjMxMjIgMTUyLjY3NiA5Ni4xNzk3QzE1Mi44MDUgOTYuMDQ3MiAxNTIuOTg3IDk1Ljk4MSAxNTMuMjI0IDk1Ljk4MUMxNTMuNDYgOTUuOTgxIDE1My42NDMgOTYuMDQ3MiAxNTMuNzcxIDk2LjE3OTdDMTUzLjkgOTYuMzEyMiAxNTMuOTY1IDk2LjQ3ODcgMTUzLjk2NSA5Ni42NzkyQzE1My45NjUgOTYuODc2MSAxNTMuOSA5Ny4wNDA5IDE1My43NzEgOTcuMTczM0MxNTMuNjQzIDk3LjMwMjIgMTUzLjQ2IDk3LjM2NjcgMTUzLjIyNCA5Ny4zNjY3QzE1Mi45ODcgOTcuMzY2NyAxNTIuODA1IDk3LjMwMjIgMTUyLjY3NiA5Ny4xNzMzQzE1Mi41NSA5Ny4wNDA5IDE1Mi40ODggOTYuODc2MSAxNTIuNDg4IDk2LjY3OTJaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTYuNjg4IDEwNEgxNTUuMzgzVjk1Ljc1SDE1Ni42ODhWMTA0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYwLjY3MyAxMDQuMTA3QzE1OS44NDYgMTA0LjEwNyAxNTkuMTc1IDEwMy44NDggMTU4LjY1OSAxMDMuMzI5QzE1OC4xNDcgMTAyLjgwNiAxNTcuODkxIDEwMi4xMTEgMTU3Ljg5MSAxMDEuMjQ1VjEwMS4wODNDMTU3Ljg5MSAxMDAuNTAzIDE1OC4wMDIgOTkuOTg2IDE1OC4yMjQgOTkuNTMxMkMxNTguNDUgOTkuMDcyOSAxNTguNzY1IDk4LjcxNjYgMTU5LjE2OSA5OC40NjI0QzE1OS41NzQgOTguMjA4MiAxNjAuMDI1IDk4LjA4MTEgMTYwLjUyMyA5OC4wODExQzE2MS4zMTQgOTguMDgxMSAxNjEuOTI1IDk4LjMzMzUgMTYyLjM1NCA5OC44Mzg0QzE2Mi43ODggOTkuMzQzMyAxNjMuMDA0IDEwMC4wNTggMTYzLjAwNCAxMDAuOTgxVjEwMS41MDhIMTU5LjIwN0MxNTkuMjQ2IDEwMS45ODggMTU5LjQwNiAxMDIuMzY3IDE1OS42ODUgMTAyLjY0NkMxNTkuOTY4IDEwMi45MjYgMTYwLjMyMiAxMDMuMDY1IDE2MC43NDkgMTAzLjA2NUMxNjEuMzQ3IDEwMy4wNjUgMTYxLjgzMyAxMDIuODI0IDE2Mi4yMDkgMTAyLjM0TDE2Mi45MTMgMTAzLjAxMkMxNjIuNjggMTAzLjM1OSAxNjIuMzY5IDEwMy42MjkgMTYxLjk3OSAxMDMuODIzQzE2MS41OTIgMTA0LjAxMyAxNjEuMTU3IDEwNC4xMDcgMTYwLjY3MyAxMDQuMTA3Wk0xNjAuNTE4IDk5LjEyODRDMTYwLjE2IDk5LjEyODQgMTU5Ljg2OSA5OS4yNTM3IDE1OS42NDcgOTkuNTA0NEMxNTkuNDI5IDk5Ljc1NSAxNTkuMjg5IDEwMC4xMDQgMTU5LjIyOSAxMDAuNTUySDE2MS43MTVWMTAwLjQ1NUMxNjEuNjg3IDEwMC4wMTggMTYxLjU3IDk5LjY4ODggMTYxLjM2NiA5OS40NjY4QzE2MS4xNjIgOTkuMjQxMiAxNjAuODc5IDk5LjEyODQgMTYwLjUxOCA5OS4xMjg0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYzLjc3OCAxMDEuMDUxQzE2My43NzggMTAwLjE1NiAxNjMuOTg2IDk5LjQzODIgMTY0LjQwMSA5OC44OTc1QzE2NC44MTYgOTguMzUzMiAxNjUuMzczIDk4LjA4MTEgMTY2LjA3MSA5OC4wODExQzE2Ni42ODcgOTguMDgxMSAxNjcuMTg1IDk4LjI5NTkgMTY3LjU2NCA5OC43MjU2Vjk1Ljc1SDE2OC44N1YxMDRIMTY3LjY4OEwxNjcuNjI0IDEwMy4zOThDMTY3LjIzMyAxMDMuODcxIDE2Ni43MTIgMTA0LjEwNyAxNjYuMDYxIDEwNC4xMDdDMTY1LjM4IDEwNC4xMDcgMTY0LjgyOSAxMDMuODMzIDE2NC40MDYgMTAzLjI4NkMxNjMuOTg3IDEwMi43MzggMTYzLjc3OCAxMDEuOTkzIDE2My43NzggMTAxLjA1MVpNMTY1LjA4MyAxMDEuMTY0QzE2NS4wODMgMTAxLjc1NSAxNjUuMTk2IDEwMi4yMTcgMTY1LjQyMSAxMDIuNTVDMTY1LjY1MSAxMDIuODc5IDE2NS45NzUgMTAzLjA0NCAxNjYuMzk0IDEwMy4wNDRDMTY2LjkyNyAxMDMuMDQ0IDE2Ny4zMTcgMTAyLjgwNiAxNjcuNTY0IDEwMi4zM1Y5OS44NDgxQzE2Ny4zMjUgOTkuMzgyNiAxNjYuOTM4IDk5LjE0OTkgMTY2LjQwNCA5OS4xNDk5QzE2NS45ODIgOTkuMTQ5OSAxNjUuNjU2IDk5LjMxODIgMTY1LjQyNyA5OS42NTQ4QzE2NS4xOTggOTkuOTg3OCAxNjUuMDgzIDEwMC40OTEgMTY1LjA4MyAxMDEuMTY0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTQwLjc5NSAxNDRWMTM2LjE4SDE0My4xMDRDMTQzLjc5NiAxMzYuMTggMTQ0LjQwOCAxMzYuMzM0IDE0NC45NDEgMTM2LjY0MkMxNDUuNDc5IDEzNi45NSAxNDUuODk0IDEzNy4zODYgMTQ2LjE4OCAxMzcuOTUyQzE0Ni40ODEgMTM4LjUxOCAxNDYuNjI4IDEzOS4xNjYgMTQ2LjYyOCAxMzkuODk2VjE0MC4yODlDMTQ2LjYyOCAxNDEuMDMgMTQ2LjQ3OSAxNDEuNjgxIDE0Ni4xODIgMTQyLjI0NEMxNDUuODg5IDE0Mi44MDYgMTQ1LjQ2OCAxNDMuMjM5IDE0NC45MiAxNDMuNTQzQzE0NC4zNzYgMTQzLjg0OCAxNDMuNzUxIDE0NCAxNDMuMDQ1IDE0NEgxNDAuNzk1Wk0xNDIuMTU0IDEzNy4yNzVWMTQyLjkxNUgxNDMuMDRDMTQzLjc1MyAxNDIuOTE1IDE0NC4yOTkgMTQyLjY5MyAxNDQuNjc4IDE0Mi4yNDlDMTQ1LjA2MSAxNDEuODAxIDE0NS4yNTcgMTQxLjE2IDE0NS4yNjQgMTQwLjMyNlYxMzkuODkxQzE0NS4yNjQgMTM5LjA0MiAxNDUuMDc5IDEzOC4zOTQgMTQ0LjcxIDEzNy45NDdDMTQ0LjM0MiAxMzcuNDk5IDE0My44MDYgMTM3LjI3NSAxNDMuMTA0IDEzNy4yNzVIMTQyLjE1NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1MC40MTUgMTQ0LjEwN0MxNDkuNTg3IDE0NC4xMDcgMTQ4LjkxNiAxNDMuODQ4IDE0OC40IDE0My4zMjlDMTQ3Ljg4OCAxNDIuODA2IDE0Ny42MzIgMTQyLjExMSAxNDcuNjMyIDE0MS4yNDVWMTQxLjA4M0MxNDcuNjMyIDE0MC41MDMgMTQ3Ljc0MyAxMzkuOTg2IDE0Ny45NjUgMTM5LjUzMUMxNDguMTkxIDEzOS4wNzMgMTQ4LjUwNiAxMzguNzE3IDE0OC45MTEgMTM4LjQ2MkMxNDkuMzE1IDEzOC4yMDggMTQ5Ljc2NiAxMzguMDgxIDE1MC4yNjQgMTM4LjA4MUMxNTEuMDU2IDEzOC4wODEgMTUxLjY2NiAxMzguMzMzIDE1Mi4wOTYgMTM4LjgzOEMxNTIuNTI5IDEzOS4zNDMgMTUyLjc0NiAxNDAuMDU4IDE1Mi43NDYgMTQwLjk4MVYxNDEuNTA4SDE0OC45NDhDMTQ4Ljk4OCAxNDEuOTg4IDE0OS4xNDcgMTQyLjM2NyAxNDkuNDI2IDE0Mi42NDZDMTQ5LjcwOSAxNDIuOTI2IDE1MC4wNjQgMTQzLjA2NSAxNTAuNDkgMTQzLjA2NUMxNTEuMDg4IDE0My4wNjUgMTUxLjU3NSAxNDIuODI0IDE1MS45NTEgMTQyLjM0TDE1Mi42NTQgMTQzLjAxMkMxNTIuNDIyIDE0My4zNTkgMTUyLjExIDE0My42MjkgMTUxLjcyIDE0My44MjNDMTUxLjMzMyAxNDQuMDEzIDE1MC44OTggMTQ0LjEwNyAxNTAuNDE1IDE0NC4xMDdaTTE1MC4yNTkgMTM5LjEyOEMxNDkuOTAxIDEzOS4xMjggMTQ5LjYxMSAxMzkuMjU0IDE0OS4zODkgMTM5LjUwNEMxNDkuMTcgMTM5Ljc1NSAxNDkuMDMxIDE0MC4xMDQgMTQ4Ljk3IDE0MC41NTJIMTUxLjQ1N1YxNDAuNDU1QzE1MS40MjggMTQwLjAxOCAxNTEuMzEyIDEzOS42ODkgMTUxLjEwNyAxMzkuNDY3QzE1MC45MDMgMTM5LjI0MSAxNTAuNjIgMTM5LjEyOCAxNTAuMjU5IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNTUuMTUyIDE0NEgxNTMuODQ3VjEzNS43NUgxNTUuMTUyVjE0NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1Ny45NjYgMTQ0SDE1Ni42NjFWMTM4LjE4OEgxNTcuOTY2VjE0NFpNMTU2LjU4MSAxMzYuNjc5QzE1Ni41ODEgMTM2LjQ3OSAxNTYuNjQzIDEzNi4zMTIgMTU2Ljc2OSAxMzYuMThDMTU2Ljg5NyAxMzYuMDQ3IDE1Ny4wOCAxMzUuOTgxIDE1Ny4zMTYgMTM1Ljk4MUMxNTcuNTUzIDEzNS45ODEgMTU3LjczNSAxMzYuMDQ3IDE1Ny44NjQgMTM2LjE4QzE1Ny45OTMgMTM2LjMxMiAxNTguMDU4IDEzNi40NzkgMTU4LjA1OCAxMzYuNjc5QzE1OC4wNTggMTM2Ljg3NiAxNTcuOTkzIDEzNy4wNDEgMTU3Ljg2NCAxMzcuMTczQzE1Ny43MzUgMTM3LjMwMiAxNTcuNTUzIDEzNy4zNjcgMTU3LjMxNiAxMzcuMzY3QzE1Ny4wOCAxMzcuMzY3IDE1Ni44OTcgMTM3LjMwMiAxNTYuNzY5IDEzNy4xNzNDMTU2LjY0MyAxMzcuMDQxIDE1Ni41ODEgMTM2Ljg3NiAxNTYuNTgxIDEzNi42NzlaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNjEuNDQxIDE0Mi4zNDZMMTYyLjY3MSAxMzguMTg4SDE2NC4wMkwxNjIuMDA1IDE0NEgxNjAuODcyTDE1OC44NDIgMTM4LjE4OEgxNjAuMTk1TDE2MS40NDEgMTQyLjM0NloiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE2Ny4zMjMgMTQ0LjEwN0MxNjYuNDk2IDE0NC4xMDcgMTY1LjgyNCAxNDMuODQ4IDE2NS4zMDkgMTQzLjMyOUMxNjQuNzk3IDE0Mi44MDYgMTY0LjU0MSAxNDIuMTExIDE2NC41NDEgMTQxLjI0NVYxNDEuMDgzQzE2NC41NDEgMTQwLjUwMyAxNjQuNjUyIDEzOS45ODYgMTY0Ljg3NCAxMzkuNTMxQzE2NS4wOTkgMTM5LjA3MyAxNjUuNDE0IDEzOC43MTcgMTY1LjgxOSAxMzguNDYyQzE2Ni4yMjMgMTM4LjIwOCAxNjYuNjc1IDEzOC4wODEgMTY3LjE3MiAxMzguMDgxQzE2Ny45NjQgMTM4LjA4MSAxNjguNTc0IDEzOC4zMzMgMTY5LjAwNCAxMzguODM4QzE2OS40MzcgMTM5LjM0MyAxNjkuNjU0IDE0MC4wNTggMTY5LjY1NCAxNDAuOTgxVjE0MS41MDhIMTY1Ljg1NkMxNjUuODk2IDE0MS45ODggMTY2LjA1NSAxNDIuMzY3IDE2Ni4zMzQgMTQyLjY0NkMxNjYuNjE3IDE0Mi45MjYgMTY2Ljk3MiAxNDMuMDY1IDE2Ny4zOTggMTQzLjA2NUMxNjcuOTk2IDE0My4wNjUgMTY4LjQ4MyAxNDIuODI0IDE2OC44NTkgMTQyLjM0TDE2OS41NjIgMTQzLjAxMkMxNjkuMzMgMTQzLjM1OSAxNjkuMDE4IDE0My42MjkgMTY4LjYyOCAxNDMuODIzQzE2OC4yNDEgMTQ0LjAxMyAxNjcuODA2IDE0NC4xMDcgMTY3LjMyMyAxNDQuMTA3Wk0xNjcuMTY3IDEzOS4xMjhDMTY2LjgwOSAxMzkuMTI4IDE2Ni41MTkgMTM5LjI1NCAxNjYuMjk3IDEzOS41MDRDMTY2LjA3OCAxMzkuNzU1IDE2NS45MzkgMTQwLjEwNCAxNjUuODc4IDE0MC41NTJIMTY4LjM2NVYxNDAuNDU1QzE2OC4zMzYgMTQwLjAxOCAxNjguMjIgMTM5LjY4OSAxNjguMDE2IDEzOS40NjdDMTY3LjgxMiAxMzkuMjQxIDE2Ny41MjkgMTM5LjEyOCAxNjcuMTY3IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNzMuNzE0IDEzOS4zODFDMTczLjU0MiAxMzkuMzUyIDE3My4zNjUgMTM5LjMzOCAxNzMuMTgzIDEzOS4zMzhDMTcyLjU4NSAxMzkuMzM4IDE3Mi4xODIgMTM5LjU2NyAxNzEuOTc0IDE0MC4wMjVWMTQ0SDE3MC42NjlWMTM4LjE4OEgxNzEuOTE1TDE3MS45NDcgMTM4LjgzOEMxNzIuMjYyIDEzOC4zMzMgMTcyLjY5OSAxMzguMDgxIDE3My4yNTggMTM4LjA4MUMxNzMuNDQ0IDEzOC4wODEgMTczLjU5OCAxMzguMTA2IDE3My43MiAxMzguMTU2TDE3My43MTQgMTM5LjM4MVoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE3Ni45OTEgMTQ0LjEwN0MxNzYuMTY0IDE0NC4xMDcgMTc1LjQ5MiAxNDMuODQ4IDE3NC45NzcgMTQzLjMyOUMxNzQuNDY1IDE0Mi44MDYgMTc0LjIwOCAxNDIuMTExIDE3NC4yMDggMTQxLjI0NVYxNDEuMDgzQzE3NC4yMDggMTQwLjUwMyAxNzQuMzE5IDEzOS45ODYgMTc0LjU0MiAxMzkuNTMxQzE3NC43NjcgMTM5LjA3MyAxNzUuMDgyIDEzOC43MTcgMTc1LjQ4NyAxMzguNDYyQzE3NS44OTEgMTM4LjIwOCAxNzYuMzQzIDEzOC4wODEgMTc2Ljg0IDEzOC4wODFDMTc3LjYzMiAxMzguMDgxIDE3OC4yNDIgMTM4LjMzMyAxNzguNjcyIDEzOC44MzhDMTc5LjEwNSAxMzkuMzQzIDE3OS4zMjIgMTQwLjA1OCAxNzkuMzIyIDE0MC45ODFWMTQxLjUwOEgxNzUuNTI0QzE3NS41NjQgMTQxLjk4OCAxNzUuNzIzIDE0Mi4zNjcgMTc2LjAwMiAxNDIuNjQ2QzE3Ni4yODUgMTQyLjkyNiAxNzYuNjQgMTQzLjA2NSAxNzcuMDY2IDE0My4wNjVDMTc3LjY2NCAxNDMuMDY1IDE3OC4xNTEgMTQyLjgyNCAxNzguNTI3IDE0Mi4zNEwxNzkuMjMgMTQzLjAxMkMxNzguOTk4IDE0My4zNTkgMTc4LjY4NiAxNDMuNjI5IDE3OC4yOTYgMTQzLjgyM0MxNzcuOTA5IDE0NC4wMTMgMTc3LjQ3NCAxNDQuMTA3IDE3Ni45OTEgMTQ0LjEwN1pNMTc2LjgzNSAxMzkuMTI4QzE3Ni40NzcgMTM5LjEyOCAxNzYuMTg3IDEzOS4yNTQgMTc1Ljk2NSAxMzkuNTA0QzE3NS43NDYgMTM5Ljc1NSAxNzUuNjA3IDE0MC4xMDQgMTc1LjU0NiAxNDAuNTUySDE3OC4wMzNWMTQwLjQ1NUMxNzguMDA0IDE0MC4wMTggMTc3Ljg4OCAxMzkuNjg5IDE3Ny42ODQgMTM5LjQ2N0MxNzcuNDc5IDEzOS4yNDEgMTc3LjE5NyAxMzkuMTI4IDE3Ni44MzUgMTM5LjEyOFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE4MC4wOTUgMTQxLjA1MUMxODAuMDk1IDE0MC4xNTYgMTgwLjMwMyAxMzkuNDM4IDE4MC43MTggMTM4Ljg5N0MxODEuMTM0IDEzOC4zNTMgMTgxLjY5IDEzOC4wODEgMTgyLjM4OSAxMzguMDgxQzE4My4wMDUgMTM4LjA4MSAxODMuNTAyIDEzOC4yOTYgMTgzLjg4MiAxMzguNzI2VjEzNS43NUgxODUuMTg3VjE0NEgxODQuMDA1TDE4My45NDEgMTQzLjM5OEMxODMuNTUxIDE0My44NzEgMTgzLjAzIDE0NC4xMDcgMTgyLjM3OCAxNDQuMTA3QzE4MS42OTggMTQ0LjEwNyAxODEuMTQ2IDE0My44MzMgMTgwLjcyNCAxNDMuMjg2QzE4MC4zMDUgMTQyLjczOCAxODAuMDk1IDE0MS45OTMgMTgwLjA5NSAxNDEuMDUxWk0xODEuNCAxNDEuMTY0QzE4MS40IDE0MS43NTUgMTgxLjUxMyAxNDIuMjE3IDE4MS43MzkgMTQyLjU1QzE4MS45NjggMTQyLjg3OSAxODIuMjkyIDE0My4wNDQgMTgyLjcxMSAxNDMuMDQ0QzE4My4yNDQgMTQzLjA0NCAxODMuNjM1IDE0Mi44MDYgMTgzLjg4MiAxNDIuMzNWMTM5Ljg0OEMxODMuNjQyIDEzOS4zODMgMTgzLjI1NSAxMzkuMTUgMTgyLjcyMiAxMzkuMTVDMTgyLjI5OSAxMzkuMTUgMTgxLjk3MyAxMzkuMzE4IDE4MS43NDQgMTM5LjY1NUMxODEuNTE1IDEzOS45ODggMTgxLjQgMTQwLjQ5MSAxODEuNCAxNDEuMTY0WiIgZmlsbD0iIzRDQUY1MCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE5NiAxSDRDMi4zNDMxNCAxIDEgMi4zNDMxNCAxIDRWMTU2QzEgMTU3LjY1NyAyLjM0MzE0IDE1OSA0IDE1OUgxOTZDMTk3LjY1NyAxNTkgMTk5IDE1Ny42NTcgMTk5IDE1NlY0QzE5OSAyLjM0MzE1IDE5Ny42NTcgMSAxOTYgMVpNNCAwSDE5NkMxOTguMjA5IDAgMjAwIDEuNzkwODYgMjAwIDRWMTU2QzIwMCAxNTguMjA5IDE5OC4yMDkgMTYwIDE5NiAxNjBINEMxLjc5MDg2IDE2MCAwIDE1OC4yMDkgMCAxNTZWNEMwIDEuNzkwODYgMS43OTA4NiAwIDQgMFoiIGZpbGw9IiNFMEUwRTAiLz4KPC9zdmc+Cg==", + "description": "Displays Persistent RPC requests that match selected alias and filter with the ability of pagination and sending persistent RPC requests.", + "descriptor": { + "type": "rpc", + "sizeX": 7.5, + "sizeY": 4, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"PersistentTableSettings\",\n \"properties\": {\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowSendRequest\": {\n \"title\": \"Allow send RPC request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display request details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowDelete\": {\n \"title\": \"Allow delete request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"displayColumns\": {\n \"title\": \"Columns for display\",\n \"type\": \"array\",\n \"minItems\": 1\n }\n },\n \"required\": [\"displayColumns\"]\n },\n \"uiSchema\": {\n \"type\": \"VerticalLayout\",\n \"elements\": [\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableStickyHeader\"\n },\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableFilter\"\n }\n ]\n },\n \"form\": [\n [\n \"enableStickyHeader\",\n \"enableFilter\",\n \"allowSendRequest\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowDelete\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ],\n [\n {\n \"key\": \"displayColumns\",\n \"type\": \"rc-select\",\n \"multiple\": true,\n \"default\": [\"rpcId\", \"messageType\", \"status\", \"method\", \"createdTime\", \"expirationTime\"],\n \"items\": [\n {\n \"value\": \"rpcId\",\n \"label\": \"RPC ID\"\n },\n {\n \"value\": \"messageType\",\n \"label\": \"Message type\"\n },\n {\n \"value\": \"status\",\n \"label\": \"Status\"\n },\n {\n \"value\": \"method\",\n \"label\": \"Method\"\n },\n {\n \"value\": \"createdTime\",\n \"label\": \"Created time\"\n },\n {\n \"value\": \"expirationTime\",\n \"label\": \"Expiration time\"\n }\n ]\n }\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Columns settings\"\n }]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" + } } ] } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 389a767cdf..844795593c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -177,8 +177,8 @@ public class RpcV2Controller extends AbstractRpcController { @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES) - @RequestParam RpcStatus rpcStatus, + @ApiParam(value = "Status of the RPC", allowableValues = RPC_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) RpcStatus rpcStatus, @ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -194,7 +194,12 @@ public class RpcV2Controller extends AbstractRpcController { accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { @Override public void onSuccess(@Nullable DeferredResult result) { - PageData rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + PageData rpcCalls; + if (rpcStatus != null) { + rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + } else { + rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink); + } response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java index 4bdb1a169d..46b82077a3 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java @@ -35,5 +35,7 @@ public interface RpcService { ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id); + PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink); + PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 02b4bbe433..a5538f56d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -82,7 +82,15 @@ public class BaseRpcService implements RpcService { log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validatePageLink(pageLink); - return rpcDao.findAllByDeviceId(tenantId, deviceId, rpcStatus, pageLink); + return rpcDao.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + } + + @Override + public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) { + log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], pageLink [{}]", tenantId, deviceId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return rpcDao.findAllByDeviceId(tenantId, deviceId, pageLink); } private PaginatedRemover tenantRpcRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java index 63af784dbb..3f34db3907 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java @@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.dao.Dao; public interface RpcDao extends Dao { - PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); + PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink); + + PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); PageData findAllRpcByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java index 221ef17361..6791de4122 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java @@ -50,7 +50,12 @@ public class JpaRpcDao extends JpaAbstractDao implements RpcDao } @Override - public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { + public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) { + return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceId(tenantId.getId(), deviceId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceIdAndStatus(tenantId.getId(), deviceId.getId(), rpcStatus, DaoUtil.toPageable(pageLink))); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java index 76b67b3823..0ca33dbb2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java @@ -26,6 +26,8 @@ import org.thingsboard.server.dao.model.sql.RpcEntity; import java.util.UUID; public interface RpcRepository extends CrudRepository { + Page findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable); + Page findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable); Page findAllByTenantId(UUID tenantId, Pageable pageable); diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index d053efb601..c0bab56e4e 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -55,6 +55,8 @@ import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -71,9 +73,9 @@ export interface WidgetSubscriptionApi { export interface RpcApi { sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string) => Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable; sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string) => Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable; completedCommand: () => void; } @@ -287,6 +289,8 @@ export interface IWidgetSubscription { comparisonEnabled?: boolean; comparisonTimeWindow?: WidgetTimewindow; + persistentRequests?: PageData; + alarms?: PageData; alarmSource?: Datasource; @@ -313,11 +317,13 @@ export interface IWidgetSubscription { updateTimewindowConfig(newTimewindow: Timewindow): void; sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; clearRpcError(): void; + subscribeForPersistentRequests(pageLink: PageLink, keyFileter: RpcStatus): Observable; + subscribe(): void; subscribeAllForPaginatedData(pageLink: EntityDataPageLink, diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 21f95a9158..c548b0fbab 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -70,6 +70,7 @@ import { import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; +import { PageLink } from '@shared/models/page/page-link'; const moment = moment_; @@ -656,13 +657,13 @@ export class WidgetSubscription implements IWidgetSubscription { } sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable { - return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, requestUUID); + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable { + return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID); } sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable { - return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, requestUUID); + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable { + return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID); } clearRpcError(): void { @@ -679,7 +680,8 @@ export class WidgetSubscription implements IWidgetSubscription { } sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number, - persistent?: boolean, persistentPollingInterval?: number, requestUUID?: string): Observable { + persistent?: boolean, persistentPollingInterval?: number, retries?: number, + additionalInfo?: any, requestUUID?: string): Observable { if (!this.rpcEnabled) { return throwError(new Error('Rpc disabled!')); } else { @@ -692,6 +694,8 @@ export class WidgetSubscription implements IWidgetSubscription { method, params, persistent, + retries, + additionalInfo, requestUUID }; if (timeout && timeout > 0) { @@ -777,6 +781,15 @@ export class WidgetSubscription implements IWidgetSubscription { } } + subscribeForPersistentRequests(pageLink: PageLink, keyFilter: RpcStatus): Observable { + if (!this.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else if (!this.targetDeviceId) { + return throwError(new Error('Target device is not set!')); + } + return this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter); + } + private extractRejectionErrorText(rejection: HttpErrorResponse) { let error = null; if (rejection.error) { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index c8ea7e65ff..9b5119d317 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -31,7 +31,7 @@ import { import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; -import { PersistentRpc } from '@shared/models/rpc.models'; +import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; @Injectable({ providedIn: 'root' @@ -143,6 +143,17 @@ export class DeviceService { return this.http.get(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config)); } + public deletePersistedRpc(rpcId: string, config?: RequestConfig) { + return this.http.delete(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config)); + } + + public getPersistedRpcRequests(deviceId: string, pageLink: PageLink, + keyFilter: RpcStatus, config?: RequestConfig): Observable> { + const rpcStatus = keyFilter ? '&rpcStatus=' + keyFilter : ''; + return this.http.get>(`/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}${rpcStatus}`, + defaultHttpOptionsFromConfig(config)); + } + public findByQuery(query: DeviceSearchQuery, config?: RequestConfig): Observable> { return this.http.post>('/api/devices', query, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html new file mode 100644 index 0000000000..7e0ae3056f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -0,0 +1,92 @@ + +
+ +

{{ 'widgets.persistent-table.add-title' | translate }}

+ + +
+ + +
+
+
+
+ + {{ 'widgets.persistent-table.message-types.' + persistentFormGroup.get('oneWayElseTwoWay').value | translate }} + +
+
+ + widgets.persistent-table.method + + + {{'widgets.persistent-table.method-error' | translate}} + + + {{'widgets.persistent-table.white-space-error' | translate}} + + + + widgets.persistent-table.retries + + +
+
+ + +
+ + + + widgets.persistent-table.additional-info + + + + + + + +
+
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss new file mode 100644 index 0000000000..e450fa4a09 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .add-dialog ::ng-deep { + + .params-json-editor, + .additional-json-editor { + .tb-json-object-panel { + margin: 0 0 16px; + } + + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts new file mode 100644 index 0000000000..c4e87c5e6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { RequestData } from '@shared/models/rpc.models'; + +@Component({ + selector: 'tb-persistent-add-dialog', + templateUrl: './persistent-add-dialog.component.html', + styleUrls: ['./persistent-add-dialog.component.scss'] +}) + +export class PersistentAddDialogComponent extends DialogComponent implements OnInit { + + public persistentFormGroup: FormGroup; + + private requestData: RequestData = { + persistentUpdated: false + }; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.persistentFormGroup = this.fb.group( + { + method: ['', [Validators.required, Validators.pattern(/^\S+$/)]], + oneWayElseTwoWay: [false], + retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]], + params: [{}], + additionalInfo: [{}] + } + ); + } + + save() { + if (this.persistentFormGroup.valid) { + this.requestData = { + persistentUpdated: true, + method: this.persistentFormGroup.get('method').value, + oneWayElseTwoWay: this.persistentFormGroup.get('oneWayElseTwoWay').value, + params: this.persistentFormGroup.get('params').value, + additionalInfo: this.persistentFormGroup.get('additionalInfo').value, + retries: this.persistentFormGroup.get('retries').value + }; + this.close(); + } + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.requestData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html new file mode 100644 index 0000000000..3a9451db67 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -0,0 +1,118 @@ + +
+ +

{{ persistentFormGroup.get('rpcId').value }}

+ + +
+ + +
+
+
+
+ + widgets.persistent-table.created-time + + + + widgets.persistent-table.expiration-time + + +
+
+ + widgets.persistent-table.message-type + + + + widgets.persistent-table.status + + + + widgets.persistent-table.method + + + + widgets.persistent-table.retries + + +
+ + + + + + {{ 'widgets.persistent-table.response' | translate }} + + + + + + + + {{ 'widgets.persistent-table.params' | translate }} + + + + + + + + + + {{ 'widgets.persistent-table.additional-info' | translate }} + + + + + + + + +
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss new file mode 100644 index 0000000000..ec456d5d9e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .rpc-dialog ::ng-deep { + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + + .tb-json-object-panel { + margin: 0 0 16px 0; + } + } + .tb-audit-log-response-data { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 100px; + border: 1px solid #c0c0c0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts new file mode 100644 index 0000000000..552175b905 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -0,0 +1,151 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { + PersistentRpc, + rpcStatusColors, + RpcStatus, + rpcStatusTranslation +} from '@shared/models/rpc.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; + +export interface PersistentDetailsDialogData { + persistentRequest: PersistentRpc; + allowDelete: boolean; +} + +@Component({ + selector: 'tb-persistent-details-dialog', + templateUrl: './persistent-details-dialog.component.html', + styleUrls: ['./persistent-details-dialog.component.scss'] +}) + +export class PersistentDetailsDialogComponent extends DialogComponent implements OnInit { + + @ViewChild('responseDataEditor', {static: true}) + responseDataEditorElmRef: ElementRef; + + public persistentFormGroup: FormGroup; + public rpcStatusColorsMap = rpcStatusColors; + public rpcStatus = RpcStatus; + public allowDelete: boolean; + + private persistentUpdated = false; + private responseData: string; + + constructor(protected store: Store, + protected router: Router, + private datePipe: DatePipe, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData, + public dialogRef: MatDialogRef, + private dialogService: DialogService, + private deviceService: DeviceService, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.allowDelete = data.allowDelete; + + this.persistentFormGroup = this.fb.group( + { + rpcId: [''], + createdTime: [''], + expirationTime: [''], + messageType: [''], + status: [''], + method: [''], + params: [''], + retries: [''], + response: [''], + additionalInfo: [null] + } + ); + this.loadPersistentFields(data.persistentRequest); + this.responseData = JSON.stringify(data.persistentRequest.response, null, 2); + } + + loadPersistentFields(request: PersistentRpc) { + this.persistentFormGroup.get('rpcId') + .patchValue(this.translate.instant('widgets.persistent-table.details-title') + request.id.id); + this.persistentFormGroup.get('createdTime') + .patchValue(this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss')); + this.persistentFormGroup.get('expirationTime') + .patchValue(this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss')); + this.persistentFormGroup.get('messageType') + .patchValue(this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway) + ); + this.persistentFormGroup.get('status') + .patchValue(this.translate.instant(rpcStatusTranslation.get(request.status))); + this.persistentFormGroup.get('method') + .patchValue(request.request.body.method); + if (isDefinedAndNotNull(request.request.retries)) { + this.persistentFormGroup.get('retries') + .patchValue(request.request.retries); + } + if (isDefinedAndNotNull(request.response)) { + this.persistentFormGroup.get('response') + .patchValue(request.response); + } + if (isDefinedAndNotNull(request.request.body.params)) { + this.persistentFormGroup.get('params') + .patchValue(JSON.parse(request.request.body.params)); + } + if (isDefinedAndNotNull(request.additionalInfo)) { + this.persistentFormGroup.get('additionalInfo') + .patchValue(request.additionalInfo); + } + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.persistentUpdated); + } + + deleteRpcRequest() { + const persistentRpc = this.data.persistentRequest; + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.persistentUpdated = true; + this.close(); + }); + } + } + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html new file mode 100644 index 0000000000..ffbcab5749 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html @@ -0,0 +1,44 @@ + +
+ + widgets.persistent-table.rpc-status-list + + + {{ 'widgets.persistent-table.rpc-search-status-all' | translate }} + + + {{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss new file mode 100644 index 0000000000..63f832d78c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts new file mode 100644 index 0000000000..d60c80ac43 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, InjectionToken } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; + +export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface PersistentFilterPanelData { + rpcStatus: RpcStatus; +} + +@Component({ + selector: 'tb-persistent-filter-panel', + templateUrl: './persistent-filter-panel.component.html', + styleUrls: ['./persistent-filter-panel.component.scss'] +}) +export class PersistentFilterPanelComponent { + + public persistentFilterFormGroup: FormGroup; + public result: PersistentFilterPanelData; + public rpcSearchStatusTranslationMap = rpcStatusTranslation; + + public persistentSearchStatuses = [ + RpcStatus.QUEUED, + RpcStatus.SENT, + RpcStatus.DELIVERED, + RpcStatus.SUCCESSFUL, + RpcStatus.TIMEOUT, + RpcStatus.EXPIRED, + RpcStatus.FAILED + ]; + + constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) + public data: PersistentFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder) { + this.persistentFilterFormGroup = this.fb.group( + { + rpcStatus: this.data.rpcStatus + } + ); + } + + update() { + this.result = { + rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value + }; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html new file mode 100644 index 0000000000..c4fc2428be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -0,0 +1,125 @@ + +
+
+
+ + + + {{ 'widgets.persistent-table.rpc-id' | translate }} + + + {{ column.id.id }} + + + + + {{ 'widgets.persistent-table.created-time' | translate }} + + + {{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.expiration-time' | translate }} + + + {{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.status' | translate }} + + + {{ rpcStatusTranslation.get(column.status) | translate }} + + + + + {{ 'widgets.persistent-table.message-type' | translate }} + + + {{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }} + + + + + {{ 'widgets.persistent-table.method' | translate }} + + + {{ column.request.body.method }} + + + + + + +
+ + + +
+
+ + + + + + +
+
+
+ + +
+ {{ noDataDisplayMessageText }} + {{ 'common.loading' | translate }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss new file mode 100644 index 0000000000..ec079f54cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + width: 100%; + height: 100%; + .tb-table-widget { + .table-container { + position: relative; + } + + .mat-table { + .mat-row { + &.invisible { + visibility: hidden; + } + } + } + + span.no-data-found { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + right: 0; + } + + .column-id { + min-width: 250px; + max-width: 250px; + width: 250px; + } + + .column-time { + min-width: 120px; + max-width: 120px; + width: 120px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts new file mode 100644 index 0000000000..8f5ccab14b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -0,0 +1,476 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + ElementRef, + Injector, + Input, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { BehaviorSubject, merge, Observable, of, ReplaySubject } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { + constructTableCssString, noDataMessage, + TableCellButtonActionDescriptor, + TableWidgetSettings +} from '@home/components/widget/lib/table-widget.models'; +import cssjs from '@core/css/css'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { hashCode, isDefined, isNumber } from '@core/utils'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { + PersistentRpc, + PersistentRpcData, RequestData, + RpcStatus, + rpcStatusColors, rpcStatusTranslation +} from '@shared/models/rpc.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; +import { DeviceService } from '@core/http/device.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + PersistentDetailsDialogComponent, + PersistentDetailsDialogData +} from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { + PERSISTENT_FILTER_PANEL_DATA, PersistentFilterPanelComponent, PersistentFilterPanelData +} from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; +import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; + +interface PersistentTableWidgetSettings extends TableWidgetSettings { + defaultSortOrder: string; + defaultPageSize: number; + displayPagination: boolean; + enableStickyAction: boolean; + enableStickyHeader: boolean; + enableFilter: boolean; + displayColumns: string[]; + displayDetails: boolean; + allowDelete: boolean; + allowSendRequest: boolean; +} + +interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor { + details?: boolean; + delete?: boolean; +} + +@Component({ + selector: 'tb-persistent-table-widget', + templateUrl: './persistent-table.component.html', + styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] +}) + +export class PersistentTableComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + private settings: PersistentTableWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private enableFilterAction = true; + private allowSendRequest = true; + private defaultPageSize = 10; + private defaultSortOrder = '-createdTime'; + private rpcStatusFilter: RpcStatus | null = null; + private displayDetails = true; + private allowDelete = true; + private displayTableColumns: string[]; + + public persistentDatasource: PersistentDatasource; + public noDataDisplayMessageText: string; + public rpcStatusColor = rpcStatusColors; + public rpcStatusTranslation = rpcStatusTranslation; + public displayPagination = true; + public enableStickyHeader = true; + public enableStickyAction = true; + public pageLink: PageLink; + public pageSizeOptions; + public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; + public displayedColumns: string[]; + + constructor(protected store: Store, + private elementRef: ElementRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private translate: TranslateService, + private dialogService: DialogService, + private deviceService: DeviceService, + private dialog: MatDialog) { + super(store); + } + + ngOnInit() { + this.ctx.$scope.persistentTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + if (this.displayPagination) { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + this.updateData(); + } + + private initializeConfig() { + + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; + this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : []; + this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; + this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; + this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; + this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true; + this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true; + + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate); + + this.displayedColumns = [...this.displayTableColumns]; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.settings.defaultSortOrder; + } + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); + this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + + + this.ctx.widgetActions = [ + { + name: 'widgets.persistent-table.add', + show: this.allowSendRequest, + icon: 'add', + onAction: $event => this.addPersistentRpcRequest($event) + }, + { + name: 'widgets.persistent-table.refresh', + show: true, + icon: 'refresh', + onAction: () => this.reloadPersistentRequests() + }, + { + name: 'widgets.persistent-table.filter', + show: this.enableFilterAction, + icon: 'filter_list', + onAction: $event => this.editFilter($event) + } + ]; + + if (this.settings.displayDetails) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.details'), + icon: 'more_horiz', + details: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.settings.allowDelete) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.delete'), + icon: 'delete', + delete: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.actionCellButtonAction.length) { + this.displayedColumns.push('actions'); + } + + this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription); + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'persistent-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private updateData() { + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); + } + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter); + this.ctx.detectChanges(); + } + + public onDataUpdated() { + this.ctx.detectChanges(); + } + + reloadPersistentRequests() { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + } + + deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.reloadPersistentRequests(); + }); + } + } + }); + } + } + + openRequestDetails($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialog.open + (PersistentDetailsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + persistentRequest: persistentRpc, + allowDelete: this.allowDelete + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.reloadPersistentRequests(); + } + } + ); + } + } + + addPersistentRpcRequest($event: Event){ + if ($event) { + $event.stopPropagation(); + } + this.dialog.open + (PersistentAddDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (requestData) => { + if (requestData.persistentUpdated) { + this.sendRequests(requestData); + } + } + ); + } + + private sendRequests(requestData: RequestData) { + let commandPromise; + if (requestData.oneWayElseTwoWay) { + commandPromise = this.ctx.controlApi.sendOneWayCommand( + requestData.method, + requestData.params, null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } else { + commandPromise = this.ctx.controlApi.sendTwoWayCommand( + requestData.method, + requestData.params, + null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } + commandPromise.subscribe( + () => { + this.reloadPersistentRequests(); + } + ); + } + + public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) { + if (actionDescriptor.details) { + this.openRequestDetails($event, persistentRpc); + } + if (actionDescriptor.delete) { + this.deleteRpcRequest($event, persistentRpc); + } + } + + private editFilter($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: PERSISTENT_FILTER_PANEL_DATA, + useValue: { + rpcStatus: this.rpcStatusFilter + } as PersistentFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result) { + const result = componentRef.instance.result; + this.rpcStatusFilter = result.rpcStatus; + this.reloadPersistentRequests(); + } + }); + this.ctx.detectChanges(); + } +} + +class PersistentDatasource implements DataSource { + + private persistentSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public dataLoading = true; + public pageData$ = this.pageDataSubject.asObservable(); + + constructor(private translate: TranslateService, + private subscription: IWidgetSubscription) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.persistentSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.persistentSubject.complete(); + this.pageDataSubject.complete(); + } + + reset() { + const pageData = emptyPageData(); + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + } + + loadPersistent(pageLink: PageLink, keyFilter: RpcStatus) { + this.dataLoading = true; + + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, keyFilter).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, keyFilter: RpcStatus): Observable> { + return this.subscription.subscribeForPersistentRequests(pageLink, keyFilter); + } + + isEmpty(): Observable { + return this.persistentSubject.pipe( + map((requests) => !requests.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts index 330429ce10..a4da8f4b82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts @@ -21,6 +21,10 @@ import { LedIndicatorComponent } from '@home/components/widget/lib/rpc/led-indic import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component'; import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component'; import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; +import { PersistentTableComponent } from '@home/components/widget/lib/rpc/persistent-table.component'; +import { PersistentDetailsDialogComponent } from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; +import { PersistentFilterPanelComponent } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; +import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; @NgModule({ declarations: @@ -28,7 +32,11 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; LedIndicatorComponent, RoundSwitchComponent, SwitchComponent, - KnobComponent + KnobComponent, + PersistentTableComponent, + PersistentDetailsDialogComponent, + PersistentAddDialogComponent, + PersistentFilterPanelComponent ], imports: [ CommonModule, @@ -38,7 +46,10 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; LedIndicatorComponent, RoundSwitchComponent, SwitchComponent, - KnobComponent + KnobComponent, + PersistentTableComponent, + PersistentDetailsDialogComponent, + PersistentAddDialogComponent ] }) export class RpcWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 2a530c8b4e..f63f53d032 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -196,16 +196,18 @@ export class WidgetContext { }; controlApi: RpcApi = { - sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => { + sendOneWayCommand: (method, params, timeout, persistent, + retries, additionalInfo, requestUUID) => { if (this.defaultSubscription) { - return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID); + return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID); } else { return of(null); } }, - sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => { + sendTwoWayCommand: (method, params, timeout, persistent, + retries, additionalInfo, requestUUID) => { if (this.defaultSubscription) { - return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID); + return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID); } else { return of(null); } diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.html b/ui-ngx/src/app/shared/components/json-object-view.component.html new file mode 100644 index 0000000000..379c557b44 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.html @@ -0,0 +1,22 @@ + +
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.scss b/ui-ngx/src/app/shared/components/json-object-view.component.scss new file mode 100644 index 0000000000..2b754b0ef2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + #tb-json-view { + width: 100%; + height: 100%; + margin-bottom: 16px; + border: 1px solid #c0c0c0; + + &:not(.fill-height) { + min-height: 100px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts new file mode 100644 index 0000000000..5ce00b0189 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -0,0 +1,166 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { RafService } from '@core/services/raf.service'; +import { isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { getAce } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-json-object-view', + templateUrl: './json-object-view.component.html', + styleUrls: ['./json-object-view.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsonObjectViewComponent), + multi: true + } + ] +}) +export class JsonObjectViewComponent implements OnInit { + + @ViewChild('jsonViewer', {static: true}) + jsonViewerElmRef: ElementRef; + + private jsonViewer: Ace.Editor; + private viewerElement: Ace.Editor; + private propagateChange = null; + private modelValue: any; + private contentValue: string; + + @Input() label: string; + + @Input() fillHeight: boolean; + + @Input() editorStyle: { [klass: string]: any }; + + @Input() sort: (key: string, value: any) => any; + + private widthValue: boolean; + + get autoWidth(): boolean { + return this.widthValue; + } + + @Input() + set autoWidth(value: boolean) { + this.widthValue = coerceBooleanProperty(value); + } + + private heigthValue: boolean; + + get autoHeight(): boolean { + return this.heigthValue; + } + + @Input() + set autoHeight(value: boolean) { + this.heigthValue = coerceBooleanProperty(value); + } + + constructor(public elementRef: ElementRef, + protected store: Store, + private raf: RafService, + private renderer: Renderer2) { + } + + ngOnInit(): void { + this.viewerElement = this.jsonViewerElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/java', + theme: 'ace/theme/github', + showGutter: false, + showPrintMargin: false, + readOnly: true + }; + + const advancedOptions = { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.jsonViewer = ace.edit(this.viewerElement, editorOptions); + this.jsonViewer.session.setUseWrapMode(false); + this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); + if (this.contentValue && (this.widthValue || this.heigthValue)) { + this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + } + } + ); + } + + updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { + let newHeight = 200; + let newWidth = 600; + if (content && content.length > 0) { + const lines = content.split('\n'); + newHeight = 17 * lines.length + 17; + let maxLineLength = 0; + lines.forEach((row) => { + const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); + const lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + }); + newWidth = 8 * maxLineLength + 16; + } + if (this.heigthValue) { + // this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + } + if (this.widthValue) { + this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + } + editor.resize(); + } + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: any): void { + this.modelValue = value; + this.contentValue = ''; + try { + if (isDefinedAndNotNull(this.modelValue)) { + this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : + (key, objectValue) => { + return this.sort(key, objectValue); + }, 2); + } + } catch (e) { + // + } + if (this.jsonViewer) { + this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); + if (this.contentValue && (this.widthValue || this.heigthValue)) { + this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + } + } + } + +} diff --git a/ui-ngx/src/app/shared/models/rpc.models.ts b/ui-ngx/src/app/shared/models/rpc.models.ts index 75ac132394..61a94a401e 100644 --- a/ui-ngx/src/app/shared/models/rpc.models.ts +++ b/ui-ngx/src/app/shared/models/rpc.models.ts @@ -17,15 +17,42 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { RpcId } from '@shared/models/id/rpc-id'; import { DeviceId } from '@shared/models/id/device-id'; +import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; export enum RpcStatus { QUEUED = 'QUEUED', DELIVERED = 'DELIVERED', SUCCESSFUL = 'SUCCESSFUL', TIMEOUT = 'TIMEOUT', - FAILED = 'FAILED' + FAILED = 'FAILED', + SENT = 'SENT', + EXPIRED = 'EXPIRED' } +export const rpcStatusColors = new Map( + [ + [RpcStatus.QUEUED, 'black'], + [RpcStatus.DELIVERED, 'green'], + [RpcStatus.SUCCESSFUL, 'green'], + [RpcStatus.TIMEOUT, 'orange'], + [RpcStatus.FAILED, 'red'], + [RpcStatus.SENT, 'green'], + [RpcStatus.EXPIRED, 'red'] + ] +); + +export const rpcStatusTranslation = new Map( + [ + [RpcStatus.QUEUED, 'widgets.persistent-table.rpc-status.QUEUED'], + [RpcStatus.DELIVERED, 'widgets.persistent-table.rpc-status.DELIVERED'], + [RpcStatus.SUCCESSFUL, 'widgets.persistent-table.rpc-status.SUCCESSFUL'], + [RpcStatus.TIMEOUT, 'widgets.persistent-table.rpc-status.TIMEOUT'], + [RpcStatus.FAILED, 'widgets.persistent-table.rpc-status.FAILED'], + [RpcStatus.SENT, 'widgets.persistent-table.rpc-status.SENT'], + [RpcStatus.EXPIRED, 'widgets.persistent-table.rpc-status.EXPIRED'] + ] +); + export interface PersistentRpc { id: RpcId; createdTime: number; @@ -34,7 +61,29 @@ export interface PersistentRpc { response: any; request: { id: string; + oneway: boolean; + body: { + method: string; + params: string; + }; + retries: null | number; }; deviceId: DeviceId; tenantId: TenantId; + additionalInfo?: string; +} + +export interface PersistentRpcData extends PersistentRpc { + actionCellButtons?: TableCellButtonActionDescriptor[]; + hasActions?: boolean; +} + +export interface RequestData { + persistentUpdated: boolean; + method?: string; + oneWayElseTwoWay?: boolean; + persistentPollingInterval?: number; + retries?: number; + params?: object; + additionalInfo?: object; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 90b80393dd..3941dd902a 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -94,6 +94,7 @@ import { SocialSharePanelComponent } from '@shared/components/socialshare-panel. import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; import { EntityListSelectComponent } from '@shared/components/entity/entity-list-select.component'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { JsonObjectViewComponent, } from '@shared/components/json-object-view.component'; import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; import { CircularProgressDirective } from '@shared/components/circular-progress.directive'; import { @@ -231,6 +232,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + JsonObjectViewComponent, JsonContentComponent, JsFuncComponent, FabTriggerDirective, @@ -376,6 +378,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + JsonObjectViewComponent, JsonContentComponent, JsFuncComponent, FabTriggerDirective, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bd14f73e27..79f8e18030 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3238,7 +3238,49 @@ "update-timeseries": "Update timeseries", "value": "Value" }, - "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type" + "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Message type", + "method": "Method", + "params": "Params", + "created-time": "Created time", + "expiration-time": "Expiration time", + "retries": "Retries", + "status": "Status", + "filter": "Filter", + "refresh": "Refresh", + "add": "Add RPC request", + "details": "Details", + "delete": "Delete", + "delete-request-title": "Delete Persistent RPC request", + "delete-request-text": "Are you sure you want to delete request?", + "details-title": "Details RPC ID: ", + "additional-info": "Additional info", + "response": "Response", + "any-status": "Any status", + "rpc-status-list": "RPC status list", + "no-request-prompt": "No request to display", + "send-request": "Send request", + "add-title": "Create Persistent RPC request", + "method-error": "Method is required.", + "timeout-error": "Min timeout value is 5000 (5 seconds).", + "white-space-error": "White space is not allowed.", + "rpc-status": { + "QUEUED": "QUEUED", + "SENT": "SENT", + "DELIVERED": "DELIVERED", + "SUCCESSFUL": "SUCCESSFUL", + "TIMEOUT": "TIMEOUT", + "EXPIRED": "EXPIRED", + "FAILED": "FAILED" + }, + "rpc-search-status-all": "ALL", + "message-types": { + "false": "Two-way", + "true": "One-way" + } + } }, "icon": { "icon": "Icon", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 66d99281bb..dfc5226d6d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -1787,6 +1787,47 @@ "update-attribute": "Обновить атрибут", "update-timeseries": "Обновить телеметрию", "value": "Значение" + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Тип сообщения", + "method": "Метод", + "params": "Параметры", + "created-time": "Время создания", + "expiration-time": "Время жизни", + "retries": "Повторные попытки", + "status": "Статус", + "filter": "Фильтр", + "refresh": "Обновить", + "add": "Добавить RPC запрос", + "details": "Детали", + "delete": "Удалить", + "delete-request-title": "Удалить RPC запрос", + "delete-request-text": "Вы точно хотите удалить RPC запрос?", + "details-title": "Детали RPC ID: ", + "additional-info": "Дополнительная информация", + "response": "Ответ", + "any-status": "Любой статус", + "rpc-status-list": "Список RPC статусов", + "no-request-prompt": "Запросы не найдены", + "send-request": "Отправить запрос", + "add-title": "Добавить новый RPC запрос", + "method-error": "Метод обязателен.", + "white-space-error": "Пробелы не допускаются.", + "rpc-status": { + "QUEUED": "В ОЧЕРЕДИ", + "SENT": "ОТПРАВЛЕННО", + "DELIVERED": "ДОСТАВЛЕННО", + "SUCCESSFUL": "УСПЕШНО", + "TIMEOUT": "ВРЕМЯ ИСТЕКЛО", + "EXPIRED": "ПРОСРОЧЕНО", + "FAILED": "НЕУДАЧНО" + }, + "rpc-search-status-all": "ВСЕ", + "message-types": { + "false": "Двусторонний", + "true": "Односторонний" + } } }, "icon": { diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 43691918dd..be31aa7375 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2359,6 +2359,47 @@ "update-attribute": "Оновити атрибут", "update-timeseries": "Оновити телеметрію", "value": "Значення" + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Тип повідомлення", + "method": "Метод", + "params": "Параметри", + "created-time": "Час створення", + "expiration-time": "Час життя", + "retries": "Повторні спроби", + "status": "Статус", + "filter": "Фільтр", + "refresh": "Оновити", + "add": "Додати RPC запит", + "details": "Деталі", + "delete": "Видалити", + "delete-request-title": "Видалити RPC запит", + "delete-request-text": "Ви впевнені, що хочете видалити RPC запит?", + "details-title": "Деталі RPC ID: ", + "additional-info": "Додаткова інформація", + "response": "Відповідь", + "any-status": "Будь-який статус", + "rpc-status-list": "Список RPC статусів", + "no-request-prompt": "Запитів не знайдено", + "send-request": "Відправити запит", + "add-title": "Додати новий RPC запит", + "method-error": "Необхідно вказати метод.", + "white-space-error": "Пробіли не допускаються.", + "rpc-status": { + "QUEUED": "В ЧЕРЗІ", + "SENT": "ВІДПРАВЛЕНО", + "DELIVERED": "ДОСТАВЛЕННО", + "SUCCESSFUL": "УСПІШНО", + "TIMEOUT": "ЧАС МИНУВ", + "EXPIRED": "ПРОСРОЧЕНО", + "FAILED": "НЕ ВДАЛО" + }, + "rpc-search-status-all": "ВСІ", + "message-types": { + "false": "Двусторонній", + "true": "Односторонній" + } } }, "white-labeling": { From 721906b9214128966c318fa2fc14536c18deef9c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 29 Nov 2021 17:46:38 +0200 Subject: [PATCH 009/403] UI: Add error message for widget --- .../src/app/core/api/widget-subscription.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index c548b0fbab..1634bb7b87 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -787,7 +787,38 @@ export class WidgetSubscription implements IWidgetSubscription { } else if (!this.targetDeviceId) { return throwError(new Error('Target device is not set!')); } - return this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter); + const rpcSubject: Subject = new Subject(); + + this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter).subscribe( + (responseBody) => { + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice( index, 1 ); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.callbacks.rpcStateChanged(this); + if (!this.executingRpcRequest || rejection.status === 504) { + this.rpcRejection = rejection; + if (rejection.status === 504) { + this.rpcErrorText = 'Request Timeout.'; + } else { + this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.rpcErrorText += '
'; + this.rpcErrorText += error; + } + } + this.callbacks.onRpcFailed(this); + } + rpcSubject.error(rejection); + } + ); + return rpcSubject.asObservable(); } private extractRejectionErrorText(rejection: HttpErrorResponse) { From 05408d357ed2cbd396a40a1dc9b16930923e687c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 11:32:32 +0200 Subject: [PATCH 010/403] UI: On tables hide page size option on mobile view --- .../attribute/attribute-table.component.html | 1 + .../attribute/attribute-table.component.ts | 23 ++++++++++-- .../entity/entities-table.component.html | 1 + .../entity/entities-table.component.ts | 21 ++++++++++- .../relation/relation-table.component.html | 3 +- .../relation/relation-table.component.ts | 35 +++++++++++++++++-- .../lib/alarms-table-widget.component.html | 3 +- .../lib/alarms-table-widget.component.ts | 15 ++++++++ .../lib/entities-table-widget.component.html | 3 +- .../lib/entities-table-widget.component.ts | 20 +++++++++++ .../timeseries-table-widget.component.html | 3 +- .../lib/timeseries-table-widget.component.ts | 15 ++++++++ 12 files changed, 133 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index ead92fda03..c0612646f3 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -196,6 +196,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize" showFirstLastButtons> , private attributeService: AttributeService, private telemetryWsService: TelemetryWebsocketService, @@ -184,7 +189,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private breakpointObserver: BreakpointObserver) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -193,6 +199,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } ngOnInit() { + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } attributeScopeChanged(attributeScope: TelemetryType) { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index c4f470bbd2..a7b15670bb 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -266,6 +266,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 49386db131..cca386570d 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -64,6 +64,8 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; @Component({ selector: 'tb-entities-table', @@ -120,6 +122,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private updateDataSubscription: Subscription; private viewInited = false; + private breakpointObserverSubscription$: Subscription; + public hidePageSize = true; + constructor(protected store: Store, public route: ActivatedRoute, public translate: TranslateService, @@ -127,7 +132,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + private componentFactoryResolver: ComponentFactoryResolver, + private breakpointObserver: BreakpointObserver) { super(store); } @@ -137,6 +143,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } else { this.init(this.route.snapshot.data.entitiesTableConfig); } + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } ngOnChanges(changes: SimpleChanges): void { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 01c2e0e4ab..b5ae8c2d52 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -165,6 +165,7 @@ + [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize"> diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index b10ed82249..2ea0ccfa12 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { PageLink } from '@shared/models/page/page-link'; import { MatPaginator } from '@angular/material/paginator'; @@ -26,7 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { EntityRelationService } from '@core/http/entity-relation.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityRelation, @@ -38,6 +47,8 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; @Component({ selector: 'tb-relation-table', @@ -65,6 +76,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; + private breakpointObserverSubscription$: Subscription; + public hidePageSize = true; + @Input() set active(active: boolean) { if (this.activeValue !== active) { @@ -100,7 +114,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn private entityRelationService: EntityRelationService, public translate: TranslateService, public dialog: MatDialog, - private dialogService: DialogService) { + private dialogService: DialogService, + private cd: ChangeDetectorRef, + private breakpointObserver: BreakpointObserver) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -111,6 +127,19 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } ngOnInit() { + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } updateColumns() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index a86da23f59..872e040bad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -147,6 +147,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 7b32d84e71..73c7ea20cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -123,6 +123,7 @@ import { } from '@home/components/widget/lib/alarm-filter-panel.component'; import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -152,6 +153,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @Input() ctx: WidgetContext; + @ViewChild('alarmWidgetContainer', {static: true}) alarmWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -168,6 +170,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public displayedColumns: string[] = []; public alarmsDatasource: AlarmsDatasource; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -177,6 +180,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private settings: AlarmsTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; + private widgetResize$: ResizeObserver; private alarmsTitlePattern: string; @@ -257,6 +261,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe( () => this.pageLink.page = 0 ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.alarmWidgetContainerRef.nativeElement); } } @@ -265,6 +277,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$ = null; } + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 74ccd53ec2..7f8ae9b9b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -106,6 +106,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index bd52f2ab9b..a89c1ebe82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -107,6 +107,7 @@ import { sortItems } from '@shared/models/page/page-link'; import { entityFields } from '@shared/models/entity.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -129,6 +130,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @Input() ctx: WidgetContext; + @ViewChild('entitiesWidgetContainer', {static: true}) entitiesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -144,6 +146,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public displayedColumns: string[] = []; public entityDatasource: EntityDatasource; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -153,6 +156,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni private settings: EntitiesTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; + private widgetResize$: ResizeObserver; private entitiesTitlePattern: string; @@ -211,6 +215,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.initializeConfig(); this.updateDatasources(); this.ctx.updateWidgetParams(); + if (this.displayPagination) { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.entitiesWidgetContainerRef.nativeElement); + } + } + + ngOnDestroy(): void { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 8c3b9084b0..236d81036c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -112,6 +112,7 @@ [pageIndex]="source.pageLink.page" [pageSize]="source.pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index de8cec8dc7..183832ae90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -71,6 +71,7 @@ import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { DatePipe } from '@angular/common'; import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -115,6 +116,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @Input() ctx: WidgetContext; + @ViewChild('timeseriesWidgetContainer', {static: true}) timeseriesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChildren(MatPaginator) paginators: QueryList; @ViewChildren(MatSort) sorts: QueryList; @@ -128,6 +130,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public sources: TimeseriesTableSource[]; public sourceIndex: number; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -150,6 +153,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private subscriptions: Subscription[] = []; private widgetTimewindowChanged$: Subscription; + private widgetResize$: ResizeObserver; private searchAction: WidgetAction = { name: 'action.search', @@ -190,6 +194,14 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI }); } ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.timeseriesWidgetContainerRef.nativeElement); } } @@ -198,6 +210,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$ = null; } + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { From 5f11fa9b6cd04370d85a7075fe00ae85880e8035 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 12:23:06 +0200 Subject: [PATCH 011/403] UI: On tables hide page size option on mobile view --- .../home/components/widget/lib/alarms-table-widget.component.ts | 2 +- .../components/widget/lib/entities-table-widget.component.ts | 2 +- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 73c7ea20cd..8f003141e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -262,7 +262,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index a89c1ebe82..2c887a1668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -217,7 +217,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 183832ae90..cc35c481a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -195,7 +195,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); From 12dcbf0e5e7e73abd3920b18dfaa74273a7f4006 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 12:37:30 +0200 Subject: [PATCH 012/403] UI: Hide pageSizeOption on persistent table widget --- .../lib/rpc/persistent-table.component.html | 3 ++- .../lib/rpc/persistent-table.component.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html index c4fc2428be..fd86fd92cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index 8f5ccab14b..f2c6f9e13f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -67,6 +67,7 @@ import { PERSISTENT_FILTER_PANEL_DATA, PersistentFilterPanelComponent, PersistentFilterPanelData } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; +import { ResizeObserver } from '@juggle/resize-observer'; interface PersistentTableWidgetSettings extends TableWidgetSettings { defaultSortOrder: string; @@ -97,6 +98,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { @Input() ctx: WidgetContext; + @ViewChild('persistentWidgetContainer', {static: true}) persistentWidgetContainerRef: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -111,6 +113,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { private displayDetails = true; private allowDelete = true; private displayTableColumns: string[]; + private widgetResize$: ResizeObserver; public persistentDatasource: PersistentDatasource; public noDataDisplayMessageText: string; @@ -123,6 +126,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { public pageSizeOptions; public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; public displayedColumns: string[]; + public hidePageSize = false; constructor(protected store: Store, private elementRef: ElementRef, @@ -143,6 +147,22 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.subscription = this.ctx.defaultSubscription; this.initializeConfig(); this.ctx.updateWidgetParams(); + if (this.displayPagination) { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.persistentWidgetContainerRef.nativeElement); + } + } + + ngOnDestroy() { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { From 33508f558e531b10801408b6b42a415ec368a96b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 2 Dec 2021 13:18:42 +0200 Subject: [PATCH 013/403] UI: Hide pageSizeOption on persistent table widget --- .../components/widget/lib/rpc/persistent-table.component.ts | 3 ++- ui-ngx/src/app/shared/models/constants.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index f2c6f9e13f..c7d4da0295 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -68,6 +68,7 @@ import { } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface PersistentTableWidgetSettings extends TableWidgetSettings { defaultSortOrder: string; @@ -149,7 +150,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.persistentWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 03b38345ca..4699a6338c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -241,6 +241,7 @@ export const contentTypesMap = new Map( ] ); +export const hidePageSizePixelValue = 550; export const customTranslationsPrefix = 'custom.'; export const i18nPrefix = 'i18n'; From 76dccab3f7326698d03f6f3c1473e35e0237d7d5 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 2 Dec 2021 13:53:00 +0200 Subject: [PATCH 014/403] UI: On tables hide page size option on mobile view --- .../attribute/attribute-table.component.html | 2 +- .../attribute/attribute-table.component.ts | 33 ++++++++++--------- .../entity/entities-table.component.html | 2 +- .../entity/entities-table.component.ts | 32 +++++++++--------- .../relation/relation-table.component.html | 2 +- .../relation/relation-table.component.ts | 33 ++++++++++--------- .../manage-widget-actions.component.html | 2 +- .../action/manage-widget-actions.component.ts | 31 +++++++++++++++-- .../lib/alarms-table-widget.component.ts | 3 +- .../lib/entities-table-widget.component.ts | 3 +- .../lib/timeseries-table-widget.component.ts | 3 +- ui-ngx/src/app/shared/models/constants.ts | 1 + 12 files changed, 91 insertions(+), 56 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index c0612646f3..493bfe3e2d 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index e4a41251d8..1bd4dea53f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -38,7 +38,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { fromEvent, merge, Subscription } from 'rxjs'; +import { fromEvent, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { @@ -84,8 +84,8 @@ import { } from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; import { deepClone } from '@core/utils'; import { Filters } from '@shared/models/query/query.models'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ @@ -168,13 +168,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @Input() entityName: string; + @ViewChild('attributeTableContainer', {static: true}) attributeTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + public hidePageSize = false; + private widgetResize$: ResizeObserver; constructor(protected store: Store, private attributeService: AttributeService, @@ -189,8 +190,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef, - private breakpointObserver: BreakpointObserver) { + private cd: ChangeDetectorRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -199,18 +199,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } ngOnInit() { - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.attributeTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.attributeTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index a7b15670bb..aa86660ebe 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -32,7 +32,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index cca386570d..1851d5e085 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -64,8 +64,8 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; @Component({ selector: 'tb-entities-table', @@ -111,6 +111,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn isDetailsOpen = false; detailsPanelOpened = new EventEmitter(); + @ViewChild('entitiesTableContainer', {static: true}) entitiesTableContainerRef: ElementRef; + @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('searchInput') searchInputField: ElementRef; @@ -122,8 +124,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private updateDataSubscription: Subscription; private viewInited = false; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + private widgetResize$: ResizeObserver; + public hidePageSize = false; constructor(protected store: Store, public route: ActivatedRoute, @@ -132,8 +134,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver, - private breakpointObserver: BreakpointObserver) { + private componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -143,18 +144,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } else { this.init(this.route.snapshot.data.entitiesTableConfig); } - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.entitiesTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.entitiesTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index b5ae8c2d52..edf5d1d04d 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index 2ea0ccfa12..8f0b503717 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -35,7 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { EntityRelationService } from '@core/http/entity-relation.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityRelation, @@ -47,8 +47,8 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ selector: 'tb-relation-table', @@ -76,8 +76,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + private widgetResize$: ResizeObserver; + public hidePageSize = false; @Input() set active(active: boolean) { @@ -105,6 +105,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } } + @ViewChild('relationTableContainer', {static: true}) relationTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -115,8 +116,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, - private cd: ChangeDetectorRef, - private breakpointObserver: BreakpointObserver) { + private cd: ChangeDetectorRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -127,18 +127,19 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } ngOnInit() { - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.relationTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.relationTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index 53523bc0cf..d0c1db566c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index d3fdd47bba..5173e00954 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { PageComponent } from '@shared/components/page.component'; @@ -43,6 +53,8 @@ import { } from '@home/components/widget/action/widget-action-dialog.component'; import { deepClone } from '@core/utils'; import { widgetType } from '@shared/models/widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; @Component({ selector: 'tb-manage-widget-actions', @@ -73,7 +85,10 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni viewsInited = false; dirtyValue = false; + public hidePageSize = false; + private widgetResize$: ResizeObserver; + @ViewChild('manageActionWidgetContainer', {static: true}) manageActionWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -85,7 +100,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private translate: TranslateService, private utils: UtilsService, private dialog: MatDialog, - private dialogs: DialogService) { + private dialogs: DialogService, + private cd: ChangeDetectorRef) { super(store); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -94,9 +110,20 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } ngOnInit(): void { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.manageActionWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.manageActionWidgetContainerRef.nativeElement); } ngOnDestroy(): void { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 8f003141e8..e12fcfcebd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -124,6 +124,7 @@ import { import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -262,7 +263,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.alarmWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 2c887a1668..32a07cecb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -108,6 +108,7 @@ import { entityFields } from '@shared/models/entity.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -217,7 +218,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.entitiesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index cc35c481a8..a5a2cbc12e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -72,6 +72,7 @@ import { DatePipe } from '@angular/common'; import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -195,7 +196,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.timeseriesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 03b38345ca..4699a6338c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -241,6 +241,7 @@ export const contentTypesMap = new Map( ] ); +export const hidePageSizePixelValue = 550; export const customTranslationsPrefix = 'custom.'; export const i18nPrefix = 'i18n'; From 73a55a2c34f1d2d818a456669f67eff76781e0ef Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 6 Dec 2021 16:24:33 +0200 Subject: [PATCH 015/403] Ignore no longer valid messages when processing strategy is completed or timed-out. --- .../actors/ruleChain/DefaultTbContext.java | 10 +++++++ .../RuleChainActorMessageProcessor.java | 28 +++++++++++++++---- .../actors/ruleChain/RuleNodeActor.java | 14 ++++++++-- .../RuleNodeActorMessageProcessor.java | 4 +-- .../actors/shared/ComponentMsgProcessor.java | 12 +++++++- .../service/queue/TbMsgPackCallback.java | 5 ++++ .../queue/TbMsgPackProcessingContext.java | 3 ++ .../thingsboard/server/common/msg/TbMsg.java | 10 ++++++- .../common/msg/queue/TbMsgCallback.java | 14 +++++++++- 9 files changed, 87 insertions(+), 13 deletions(-) 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 38d6f4bccb..609c087a51 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 @@ -167,6 +167,11 @@ class DefaultTbContext implements TbContext { } private void enqueue(TopicPartitionInfo tpi, TbMsg tbMsg, Consumer onFailure, Runnable onSuccess) { + if (!tbMsg.isValid()) { + log.trace("[{}] Skip invalid message: {}", getTenantId(), tbMsg); + onFailure.accept(new IllegalArgumentException("Source message is no longer valid!")); + return; + } TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) @@ -235,6 +240,11 @@ class DefaultTbContext implements TbContext { } private void enqueueForTellNext(TopicPartitionInfo tpi, String queueName, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { + if (!source.isValid()) { + log.trace("[{}] Skip invalid message: {}", getTenantId(), source); + onFailure.accept(new IllegalArgumentException("Source message is no longer valid!")); + return; + } RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 88a4f6a202..6f6ea8fcec 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -200,17 +200,20 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor relationTypes, String failureMessage) { try { - checkActive(msg); + checkComponentStateActive(msg); EntityId entityId = msg.getOriginator(); TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, msg.getQueueName(), tenantId, entityId); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java index 8fef00c49c..60f462f4d4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; @@ -86,12 +87,19 @@ public class RuleNodeActor extends ComponentActor extends Abstract schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency); } - protected void checkActive(TbMsg tbMsg) throws RuleNodeException { + protected boolean checkMsgValid(TbMsg tbMsg) { + var valid = tbMsg.isValid(); + if (!valid) { + if (log.isTraceEnabled()) { + log.trace("Skip processing of message: {} because it is no longer valid!", tbMsg); + } + } + return valid; + } + + protected void checkComponentStateActive(TbMsg tbMsg) throws RuleNodeException { if (state != ComponentLifecycleState.ACTIVE) { log.debug("Component is not active. Current state [{}] for processor [{}][{}] tenant [{}]", state, entityId.getEntityType(), entityId, tenantId); RuleNodeException ruleNodeException = getInactiveException(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java index eff1ecaf86..e3ce0927ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java @@ -66,6 +66,11 @@ public class TbMsgPackCallback implements TbMsgCallback { ctx.onFailure(tenantId, id, e); } + @Override + public boolean isMsgValid() { + return !ctx.isComplete(); + } + @Override public void onProcessingStart(RuleNodeInfo ruleNodeInfo) { log.trace("[{}] ON PROCESSING START: {}", id, ruleNodeInfo); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java index d7a064c4ca..62285a411c 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java @@ -53,6 +53,8 @@ public class TbMsgPackProcessingContext { private final ConcurrentMap exceptionsMap = new ConcurrentHashMap<>(); private final ConcurrentMap lastRuleNodeMap = new ConcurrentHashMap<>(); + @Getter + private volatile boolean complete = false; public TbMsgPackProcessingContext(String queueName, TbRuleEngineSubmitStrategy submitStrategy) { this.queueName = queueName; @@ -149,6 +151,7 @@ public class TbMsgPackProcessingContext { } public void cleanup() { + complete = true; pendingMap.clear(); successMap.clear(); failedMap.clear(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 63c148b951..7c77d8a06b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -269,7 +269,7 @@ public final class TbMsg implements Serializable { } public TbMsgCallback getCallback() { - //May be null in case of deserialization; + // May be null in case of deserialization; if (callback != null) { return callback; } else { @@ -288,4 +288,12 @@ public final class TbMsg implements Serializable { public TbMsgProcessingStackItem popFormStack() { return ctx.pop(); } + + /** + * Checks if the message is still valid for processing. May be invalid if the message pack is timed-out or canceled. + * @return 'true' if message is valid for processing, 'false' otherwise. + */ + public boolean isValid() { + return getCallback().isMsgValid(); + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java index fd62df83c9..2bab1e8340 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TbMsgCallback.java @@ -17,6 +17,9 @@ package org.thingsboard.server.common.msg.queue; import org.thingsboard.server.common.data.id.RuleNodeId; +/** + * Should be renamed to TbMsgPackContext, but this can't be changed due to backward-compatibility. + */ public interface TbMsgCallback { TbMsgCallback EMPTY = new TbMsgCallback() { @@ -36,11 +39,20 @@ public interface TbMsgCallback { void onFailure(RuleEngineException e); + /** + * Returns 'true' if rule engine is expecting the message to be processed, 'false' otherwise. + * message may no longer be valid, if the message pack is already expired/canceled/failed. + * + * @return 'true' if rule engine is expecting the message to be processed, 'false' otherwise. + */ + default boolean isMsgValid() { + return true; + } + default void onProcessingStart(RuleNodeInfo ruleNodeInfo) { } default void onProcessingEnd(RuleNodeId ruleNodeId) { } - } From dffa84483e63fbc25e43c21ea78caa19ac43c50a Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 8 Dec 2021 10:50:45 +0200 Subject: [PATCH 016/403] Fixed tests --- .../rules/flow/AbstractRuleEngineFlowIntegrationTest.java | 2 ++ .../lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java | 1 + 2 files changed, 3 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 90a0404751..eb79bc9d35 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -144,6 +144,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule Thread.sleep(1000); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); + Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system @@ -256,6 +257,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule Thread.sleep(1000); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); + Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index cfa546cc11..62b2034b79 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -135,6 +135,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac Thread.sleep(1000); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); + Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system From 09c8f2bdc7fabe3850bda97fa32ecdd5b91857bd Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 14 Dec 2021 12:40:18 +0200 Subject: [PATCH 017/403] Hide page size on tables refactoring --- .../attribute/attribute-table.component.html | 2 +- .../attribute/attribute-table.component.scss | 1 + .../attribute/attribute-table.component.ts | 18 ++++++++------- .../entity/entities-table.component.html | 2 +- .../entity/entities-table.component.scss | 1 + .../entity/entities-table.component.ts | 22 ++++++++----------- .../relation/relation-table.component.html | 2 +- .../relation/relation-table.component.scss | 1 + .../relation/relation-table.component.ts | 12 +++++----- .../manage-widget-actions.component.html | 5 +++-- .../manage-widget-actions.component.scss | 1 + .../action/manage-widget-actions.component.ts | 16 +++++++------- .../lib/alarms-table-widget.component.html | 2 +- .../lib/alarms-table-widget.component.scss | 1 + .../lib/alarms-table-widget.component.ts | 13 ++++++----- .../lib/entities-table-widget.component.html | 2 +- .../lib/entities-table-widget.component.scss | 1 + .../lib/entities-table-widget.component.ts | 13 ++++++----- .../timeseries-table-widget.component.html | 2 +- .../timeseries-table-widget.component.scss | 1 + .../lib/timeseries-table-widget.component.ts | 13 ++++++----- 21 files changed, 70 insertions(+), 61 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 493bfe3e2d..c0612646f3 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss index df6b8e7184..715eb71fbc 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 1bd4dea53f..f197f995a6 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -111,6 +111,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI pageLink: PageLink; textSearchMode = false; dataSource: AttributeDatasource; + hidePageSize = false; activeValue = false; dirtyValue = false; @@ -129,10 +130,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI aliasController: IAliasController; private widgetDatasource: Datasource; + private widgetResize$: ResizeObserver; + private disableAttributeScopeSelectionValue: boolean; + get disableAttributeScopeSelection(): boolean { return this.disableAttributeScopeSelectionValue; } + @Input() set disableAttributeScopeSelection(value: boolean) { this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value); @@ -168,15 +173,11 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @Input() entityName: string; - @ViewChild('attributeTableContainer', {static: true}) attributeTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - public hidePageSize = false; - private widgetResize$: ResizeObserver; - constructor(protected store: Store, private attributeService: AttributeService, private telemetryWsService: TelemetryWebsocketService, @@ -190,7 +191,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -200,13 +202,13 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI ngOnInit() { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.attributeTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.attributeTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index aa86660ebe..a7b15670bb 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -32,7 +32,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss index ac7648d07a..dde91efc26 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 1851d5e085..79166bb116 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -43,7 +43,8 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; import { - CellActionDescriptor, CellActionDescriptorType, + CellActionDescriptor, + CellActionDescriptorType, EntityActionTableColumn, EntityColumn, EntityTableColumn, @@ -55,11 +56,7 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { - calculateIntervalStartEndTime, - HistoryWindowType, - Timewindow -} from '@shared/models/time/time.models'; +import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; @@ -100,6 +97,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn defaultPageSize = 10; displayPagination = true; + hidePageSize = false; pageSizeOptions; pageLink: PageLink; textSearchMode = false; @@ -111,8 +109,6 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn isDetailsOpen = false; detailsPanelOpened = new EventEmitter(); - @ViewChild('entitiesTableContainer', {static: true}) entitiesTableContainerRef: ElementRef; - @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('searchInput') searchInputField: ElementRef; @@ -125,7 +121,6 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private viewInited = false; private widgetResize$: ResizeObserver; - public hidePageSize = false; constructor(protected store: Store, public route: ActivatedRoute, @@ -134,7 +129,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + private componentFactoryResolver: ComponentFactoryResolver, + private elementRef: ElementRef) { super(store); } @@ -145,13 +141,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.init(this.route.snapshot.data.entitiesTableConfig); } this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.entitiesTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.entitiesTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index edf5d1d04d..b5ae8c2d52 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index dfb94a7383..b5c43647e3 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index 8f0b503717..db1fad53b7 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -67,6 +67,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn displayedColumns: string[]; direction: EntitySearchDirection; pageLink: PageLink; + hidePageSize = false; textSearchMode = false; dataSource: RelationsDatasource; @@ -77,7 +78,6 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; private widgetResize$: ResizeObserver; - public hidePageSize = false; @Input() set active(active: boolean) { @@ -105,7 +105,6 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } } - @ViewChild('relationTableContainer', {static: true}) relationTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -116,7 +115,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -128,13 +128,13 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn ngOnInit() { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.relationTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.relationTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index d0c1db566c..9512c58b4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -119,6 +119,7 @@ + [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize">
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss index d71ec2e0d1..3406215b33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index 5173e00954..f131b84783 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -46,13 +46,12 @@ import { WidgetActionsDatasource } from '@home/components/widget/action/manage-widget-actions.component.models'; import { UtilsService } from '@core/services/utils.service'; -import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; +import { WidgetActionDescriptor, WidgetActionSource, widgetType } from '@shared/models/widget.models'; import { WidgetActionDialogComponent, WidgetActionDialogData } from '@home/components/widget/action/widget-action-dialog.component'; import { deepClone } from '@core/utils'; -import { widgetType } from '@shared/models/widget.models'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; @@ -81,14 +80,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni displayedColumns: string[]; pageLink: PageLink; textSearchMode = false; + hidePageSize = false; dataSource: WidgetActionsDatasource; viewsInited = false; dirtyValue = false; - public hidePageSize = false; + private widgetResize$: ResizeObserver; - @ViewChild('manageActionWidgetContainer', {static: true}) manageActionWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -101,7 +100,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private utils: UtilsService, private dialog: MatDialog, private dialogs: DialogService, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -111,13 +111,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni ngOnInit(): void { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.manageActionWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.manageActionWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 872e040bad..a2b0868370 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss index edc52a9c48..15664c4750 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { .table-container { position: relative; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index e12fcfcebd..5cd0f9643d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -154,7 +155,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @Input() ctx: WidgetContext; - @ViewChild('alarmWidgetContainer', {static: true}) alarmWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -167,11 +167,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public pageLink: AlarmDataPageLink; public sortOrderProperty: string; public textSearchMode = false; + public hidePageSize = false; public columns: Array = []; public displayedColumns: string[] = []; public alarmsDatasource: AlarmsDatasource; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -240,7 +240,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private datePipe: DatePipe, private dialog: MatDialog, private dialogService: DialogService, - private alarmService: AlarmService) { + private alarmService: AlarmService, + private cd: ChangeDetectorRef) { super(store); this.pageLink = { page: 0, @@ -263,13 +264,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.alarmWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.alarmWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 7f8ae9b9b1..c2d1997945 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss index edc52a9c48..15664c4750 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { .table-container { position: relative; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 32a07cecb8..f4429fb4cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, Injector, @@ -131,7 +132,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @Input() ctx: WidgetContext; - @ViewChild('entitiesWidgetContainer', {static: true}) entitiesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -143,11 +143,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public pageLink: EntityDataPageLink; public sortOrderProperty: string; public textSearchMode = false; + public hidePageSize = false; public columns: Array = []; public displayedColumns: string[] = []; public entityDatasource: EntityDatasource; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -198,7 +198,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni private utils: UtilsService, private datePipe: DatePipe, private translate: TranslateService, - private domSanitizer: DomSanitizer) { + private domSanitizer: DomSanitizer, + private cd: ChangeDetectorRef) { super(store); this.pageLink = { page: 0, @@ -218,13 +219,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.entitiesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.entitiesWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 236d81036c..6e37d38762 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss index 57052d4bf8..eb309c2cf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { mat-footer-row, mat-row { height: 38px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index a5a2cbc12e..07d35a1405 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, Input, @@ -117,7 +118,6 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @Input() ctx: WidgetContext; - @ViewChild('timeseriesWidgetContainer', {static: true}) timeseriesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChildren(MatPaginator) paginators: QueryList; @ViewChildren(MatSort) sorts: QueryList; @@ -127,11 +127,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public enableStickyAction = true; public pageSizeOptions; public textSearchMode = false; + public hidePageSize = false; public textSearch: string = null; public sources: TimeseriesTableSource[]; public sourceIndex: number; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -172,7 +172,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private utils: UtilsService, private translate: TranslateService, private domSanitizer: DomSanitizer, - private datePipe: DatePipe) { + private datePipe: DatePipe, + private cd: ChangeDetectorRef) { super(store); } @@ -196,13 +197,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.timeseriesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.timeseriesWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } From 452ea5f0e42168b7e212cbae92fa98c48528cb3b Mon Sep 17 00:00:00 2001 From: zbeacon Date: Thu, 16 Dec 2021 16:37:32 +0200 Subject: [PATCH 018/403] Init commit for notifications to gateway --- .../server/controller/DeviceController.java | 4 + .../DefaultGatewayDeviceStateService.java | 137 ++++++++++++++++++ .../GatewayDeviceStateService.java | 10 ++ .../queue/DefaultTbClusterService.java | 3 + .../transport/DefaultTransportApiService.java | 13 ++ .../server/common/data/DataConstants.java | 1 + 6 files changed, 168 insertions(+) create mode 100644 application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 6ea6bdd880..9674e8eba9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -74,6 +74,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.device.DeviceBulkImportService; +import org.thingsboard.server.service.gateway_device.GatewayDeviceStateService; import org.thingsboard.server.service.importing.BulkImportRequest; import org.thingsboard.server.service.importing.BulkImportResult; import org.thingsboard.server.service.security.model.SecurityUser; @@ -128,6 +129,8 @@ public class DeviceController extends BaseController { private final DeviceBulkImportService deviceBulkImportService; + private final GatewayDeviceStateService gatewayDeviceStatusService; + @ApiOperation(value = "Get Device (getDeviceById)", notes = "Fetch the Device object based on the provided Device Id. " + "If the user has the authority of 'TENANT_ADMIN', the server checks that the device is owned by the same tenant. " + @@ -261,6 +264,7 @@ public class DeviceController extends BaseController { List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), deviceId); + gatewayDeviceStatusService.delete(device); deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId); tbClusterService.onDeviceDeleted(device, null); diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java new file mode 100644 index 0000000000..7d9168e358 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java @@ -0,0 +1,137 @@ +package org.thingsboard.server.service.gateway_device; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultGatewayDeviceStateService implements GatewayDeviceStateService { + + private static final String RENAMED_GATEWAY_DEVICES = "renamedGatewayDevices"; + + @Lazy + @Autowired + private TelemetrySubscriptionService tsSubService; + private final AttributesService attributesService; + private final RelationService relationService; + + @Override + public void update(Device device, Device oldDevice) { + List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); + if (!relationToGatewayList.isEmpty()) { + EntityRelation relationToGateway = relationToGatewayList.get(0); + ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); + DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { + ObjectNode renamedGatewayDevicesNode; + KvEntry renamedGatewayDevicesKvEntry; + String newDeviceName = device.getName(); + String oldDeviceName = oldDevice.getName(); + + if (renamedGatewayDevicesList.isEmpty()) { + renamedGatewayDevicesNode = JacksonUtil.newObjectNode(); + renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); + } else { + AttributeKvEntry receivedRenamedGatewayDevicesAttribute = renamedGatewayDevicesList.get(0); + renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(receivedRenamedGatewayDevicesAttribute.getValueAsString()); + if (renamedGatewayDevicesNode.findValue(newDeviceName) != null) { + // If new device name is the same like the first name + renamedGatewayDevicesNode.remove(newDeviceName); + } else if (renamedGatewayDevicesNode.findValue(oldDeviceName) == null) { + + AtomicBoolean renamedFirstTime = new AtomicBoolean(true); + + renamedGatewayDevicesNode.fields().forEachRemaining(entry -> { + // If device was renamed earlier + if (oldDeviceName.equals(entry.getValue().asText())) { + renamedGatewayDevicesNode.put(entry.getKey(), newDeviceName); + renamedFirstTime.set(false); + } + }); + if (renamedFirstTime.get()) { + renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); + } + } + } + + renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + }, + e -> log.error("Cannot get gateway renamed devices attribute", e)); + } + } + + @Override + public void delete(Device device) { + List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); + if (!relationToGatewayList.isEmpty()) { + EntityRelation relationToGateway = relationToGatewayList.get(0); + String deletedDeviceName = device.getName(); + ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); + DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { + if (!renamedGatewayDevicesList.isEmpty()) { + ObjectNode renamedGatewayDevicesNode = JacksonUtil.fromString(renamedGatewayDevicesList.get(0).getValueAsString(), ObjectNode.class); + if (renamedGatewayDevicesNode != null && renamedGatewayDevicesNode.findValue(deletedDeviceName) != null) { + renamedGatewayDevicesNode.remove(deletedDeviceName); + KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + } + } + }, e -> log.error("Cannot get gateway renamed devices attribute", e)); + ListenableFuture> deletedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("deletedGatewayDevices")); + DonAsynchron.withCallback(deletedGatewayDevicesFuture, deletedGatewayDevicesList -> { + ArrayNode deletedGatewayDevicesNode; + if (!deletedGatewayDevicesList.isEmpty()) { + deletedGatewayDevicesNode = (ArrayNode) JacksonUtil.toJsonNode(deletedGatewayDevicesList.get(0).getValueAsString()); + } else { + deletedGatewayDevicesNode = JacksonUtil.OBJECT_MAPPER.createArrayNode(); + } + deletedGatewayDevicesNode.add(deletedDeviceName); + KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + }, e -> log.error("Cannot get gateway deleted devices attribute", e)); + } + } + + private void saveGatewayDevicesAttribute(Device device, EntityRelation relationToGateway, KvEntry renamedGatewayDevicesKvEntry) { + AttributeKvEntry attrKvEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), renamedGatewayDevicesKvEntry); + tsSubService.saveAndNotify(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, List.of(attrKvEntry), true, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void unused) { + log.trace("Attribute saved for gateway with ID [{}] and data [{}]", relationToGateway.getTo(), renamedGatewayDevicesKvEntry.getJsonValue()); + } + + @Override + public void onFailure(Throwable throwable) { + log.error("Cannot save gateway device attribute", throwable); + } + }); + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java new file mode 100644 index 0000000000..f086b25d5a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java @@ -0,0 +1,10 @@ +package org.thingsboard.server.service.gateway_device; + +import org.thingsboard.server.common.data.Device; + +public interface GatewayDeviceStateService { + + void update(Device device, Device oldDevice); + + void delete(Device device); +} diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 0a82994a42..be44c9ee40 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -61,6 +61,7 @@ import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.service.gateway_device.GatewayDeviceStateService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; @@ -91,6 +92,7 @@ public class DefaultTbClusterService implements TbClusterService { private final DataDecodingEncodingService encodingService; private final TbDeviceProfileCache deviceProfileCache; private final OtaPackageStateService otaPackageStateService; + private final GatewayDeviceStateService gatewayDeviceStateService; @Override public void pushMsgToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, TbQueueCallback callback) { @@ -405,6 +407,7 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(device.getTenantId(), device.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); sendDeviceStateServiceEvent(device.getTenantId(), device.getId(), created, !created, false); otaPackageStateService.update(device, old); + gatewayDeviceStateService.update(device, old); if (!created && notifyEdge) { sendNotificationMsgToEdgeService(device.getTenantId(), null, device.getId(), null, null, EdgeEventActionType.UPDATED); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 1cefefd5d1..cb4e01032f 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; @@ -99,6 +100,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.state.DeviceStateService; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -304,6 +306,17 @@ public class DefaultTransportApiService implements TransportApiService { TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, mapper.writeValueAsString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } + + List currentLastConnectedGatewayRelationList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); + EntityRelation lastConnectedGatewayRelation; + if (!currentLastConnectedGatewayRelationList.isEmpty()) { + lastConnectedGatewayRelation = currentLastConnectedGatewayRelationList.get(0); + lastConnectedGatewayRelation.setTo(gateway.getId()); + } else { + lastConnectedGatewayRelation = new EntityRelation(device.getId(), gateway.getId(), DataConstants.LAST_CONNECTED_GATEWAY); + } + relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, lastConnectedGatewayRelation); + GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() .setDeviceInfo(getDeviceInfoProto(device)); DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 707a50dfb3..40f6c13794 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -116,4 +116,5 @@ public class DataConstants { public static final String EDGE_MSG_SOURCE = "edge"; public static final String MSG_SOURCE_KEY = "source"; + public static final String LAST_CONNECTED_GATEWAY = "lastConnectedGateway"; } From 1ebf7f703906081ea7dd0ce41f2df92ed671a0bc Mon Sep 17 00:00:00 2001 From: zbeacon Date: Fri, 17 Dec 2021 10:57:46 +0200 Subject: [PATCH 019/403] Refactoring of removing devices --- .../DefaultGatewayDeviceStateService.java | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java index 7d9168e358..f3aee5a109 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java @@ -1,5 +1,6 @@ package org.thingsboard.server.service.gateway_device; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; @@ -25,9 +26,10 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @@ -36,12 +38,12 @@ import java.util.concurrent.atomic.AtomicBoolean; public class DefaultGatewayDeviceStateService implements GatewayDeviceStateService { private static final String RENAMED_GATEWAY_DEVICES = "renamedGatewayDevices"; - + private static final String DELETED_GATEWAY_DEVICES = "deletedGatewayDevices"; + private final AttributesService attributesService; + private final RelationService relationService; @Lazy @Autowired private TelemetrySubscriptionService tsSubService; - private final AttributesService attributesService; - private final RelationService relationService; @Override public void update(Device device, Device oldDevice) { @@ -61,10 +63,10 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi } else { AttributeKvEntry receivedRenamedGatewayDevicesAttribute = renamedGatewayDevicesList.get(0); renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(receivedRenamedGatewayDevicesAttribute.getValueAsString()); - if (renamedGatewayDevicesNode.findValue(newDeviceName) != null) { - // If new device name is the same like the first name + if (renamedGatewayDevicesNode.findValue(newDeviceName) != null && oldDeviceName.equals(renamedGatewayDevicesNode.get(newDeviceName).asText())) { + // If a new device name is the same like the first name or another device was renamed like some existing device renamedGatewayDevicesNode.remove(newDeviceName); - } else if (renamedGatewayDevicesNode.findValue(oldDeviceName) == null) { + } else { AtomicBoolean renamedFirstTime = new AtomicBoolean(true); @@ -93,15 +95,31 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); if (!relationToGatewayList.isEmpty()) { EntityRelation relationToGateway = relationToGatewayList.get(0); - String deletedDeviceName = device.getName(); + final String[] deletedDeviceName = {device.getName()}; ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { if (!renamedGatewayDevicesList.isEmpty()) { - ObjectNode renamedGatewayDevicesNode = JacksonUtil.fromString(renamedGatewayDevicesList.get(0).getValueAsString(), ObjectNode.class); - if (renamedGatewayDevicesNode != null && renamedGatewayDevicesNode.findValue(deletedDeviceName) != null) { - renamedGatewayDevicesNode.remove(deletedDeviceName); - KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + ObjectNode renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(renamedGatewayDevicesList.get(0).getValueAsString()); + if (renamedGatewayDevicesNode != null) { + AtomicBoolean renamedListChanged = new AtomicBoolean(false); + if (renamedGatewayDevicesNode.findValue(deletedDeviceName[0]) != null) { + renamedGatewayDevicesNode.remove(deletedDeviceName[0]); + renamedListChanged.set(true); + } else { + Map renamedGatewayDevicesMap = JacksonUtil.OBJECT_MAPPER.convertValue(renamedGatewayDevicesNode, new TypeReference<>() {}); + renamedGatewayDevicesMap.forEach((key, value) -> { + // If device was renamed earlier + if (deletedDeviceName[0].equals(value)) { + renamedGatewayDevicesNode.remove(key); + deletedDeviceName[0] = key; + renamedListChanged.set(true); + } + }); + } + if (renamedListChanged.get()) { + KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + } } } }, e -> log.error("Cannot get gateway renamed devices attribute", e)); @@ -113,19 +131,19 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi } else { deletedGatewayDevicesNode = JacksonUtil.OBJECT_MAPPER.createArrayNode(); } - deletedGatewayDevicesNode.add(deletedDeviceName); - KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + deletedGatewayDevicesNode.add(deletedDeviceName[0]); + KvEntry deletedGatewayDevicesKvEntry = new JsonDataEntry(DELETED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, deletedGatewayDevicesKvEntry); }, e -> log.error("Cannot get gateway deleted devices attribute", e)); } } - private void saveGatewayDevicesAttribute(Device device, EntityRelation relationToGateway, KvEntry renamedGatewayDevicesKvEntry) { - AttributeKvEntry attrKvEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), renamedGatewayDevicesKvEntry); + private void saveGatewayDevicesAttribute(Device device, EntityRelation relationToGateway, KvEntry gatewayDevicesKvEntry) { + AttributeKvEntry attrKvEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), gatewayDevicesKvEntry); tsSubService.saveAndNotify(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, List.of(attrKvEntry), true, new FutureCallback() { @Override public void onSuccess(@Nullable Void unused) { - log.trace("Attribute saved for gateway with ID [{}] and data [{}]", relationToGateway.getTo(), renamedGatewayDevicesKvEntry.getJsonValue()); + log.trace("Attribute saved for gateway with ID [{}] and data [{}]", relationToGateway.getTo(), gatewayDevicesKvEntry.getJsonValue()); } @Override From b76088892b1ca3d5a047c14172d0cf5320afd529 Mon Sep 17 00:00:00 2001 From: zbeacon Date: Fri, 17 Dec 2021 13:49:05 +0200 Subject: [PATCH 020/403] Added "handle device renaming parameter to the gateway" --- .../DefaultGatewayDeviceStateService.java | 151 ++++++++++++------ .../GatewayDeviceStateService.java | 17 ++ .../transport/DefaultTransportApiService.java | 4 + .../device-wizard-dialog.component.html | 6 + .../wizard/device-wizard-dialog.component.ts | 2 + .../home/pages/device/device.component.html | 6 + .../home/pages/device/device.component.ts | 2 + .../assets/locale/locale.constant-en_US.json | 1 + 8 files changed, 143 insertions(+), 46 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java index f3aee5a109..57a0b36508 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java @@ -1,6 +1,22 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.service.gateway_device; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; @@ -15,6 +31,7 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -23,11 +40,11 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -39,8 +56,11 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi private static final String RENAMED_GATEWAY_DEVICES = "renamedGatewayDevices"; private static final String DELETED_GATEWAY_DEVICES = "deletedGatewayDevices"; + private static final String HANDLE_DEVICE_RENAMING_PARAMETER = "handleDeviceRenaming"; + private final AttributesService attributesService; private final RelationService relationService; + private final DeviceService deviceService; @Lazy @Autowired private TelemetrySubscriptionService tsSubService; @@ -49,44 +69,50 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi public void update(Device device, Device oldDevice) { List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); if (!relationToGatewayList.isEmpty()) { - EntityRelation relationToGateway = relationToGatewayList.get(0); - ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); - DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { - ObjectNode renamedGatewayDevicesNode; - KvEntry renamedGatewayDevicesKvEntry; - String newDeviceName = device.getName(); - String oldDeviceName = oldDevice.getName(); - - if (renamedGatewayDevicesList.isEmpty()) { - renamedGatewayDevicesNode = JacksonUtil.newObjectNode(); - renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); - } else { - AttributeKvEntry receivedRenamedGatewayDevicesAttribute = renamedGatewayDevicesList.get(0); - renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(receivedRenamedGatewayDevicesAttribute.getValueAsString()); - if (renamedGatewayDevicesNode.findValue(newDeviceName) != null && oldDeviceName.equals(renamedGatewayDevicesNode.get(newDeviceName).asText())) { - // If a new device name is the same like the first name or another device was renamed like some existing device - renamedGatewayDevicesNode.remove(newDeviceName); - } else { - - AtomicBoolean renamedFirstTime = new AtomicBoolean(true); - - renamedGatewayDevicesNode.fields().forEachRemaining(entry -> { - // If device was renamed earlier - if (oldDeviceName.equals(entry.getValue().asText())) { - renamedGatewayDevicesNode.put(entry.getKey(), newDeviceName); - renamedFirstTime.set(false); - } - }); - if (renamedFirstTime.get()) { + if (oldDevice != null) { + EntityRelation relationToGateway = relationToGatewayList.get(0); + + Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + if (isHandleDeviceRenamingEnabled(gatewayDevice.getAdditionalInfo())) { + ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); + DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { + ObjectNode renamedGatewayDevicesNode; + KvEntry renamedGatewayDevicesKvEntry; + String newDeviceName = device.getName(); + String oldDeviceName = oldDevice.getName(); + + if (renamedGatewayDevicesList.isEmpty()) { + renamedGatewayDevicesNode = JacksonUtil.newObjectNode(); renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); + } else { + AttributeKvEntry receivedRenamedGatewayDevicesAttribute = renamedGatewayDevicesList.get(0); + renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(receivedRenamedGatewayDevicesAttribute.getValueAsString()); + if (renamedGatewayDevicesNode.findValue(newDeviceName) != null && oldDeviceName.equals(renamedGatewayDevicesNode.get(newDeviceName).asText())) { + // If a new device name is the same like the first name or another device was renamed like some existing device + renamedGatewayDevicesNode.remove(newDeviceName); + } else { + + AtomicBoolean renamedFirstTime = new AtomicBoolean(true); + + renamedGatewayDevicesNode.fields().forEachRemaining(entry -> { + // If device was renamed earlier + if (oldDeviceName.equals(entry.getValue().asText())) { + renamedGatewayDevicesNode.put(entry.getKey(), newDeviceName); + renamedFirstTime.set(false); + } + }); + if (renamedFirstTime.get()) { + renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); + } + } } - } - } - renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); - }, - e -> log.error("Cannot get gateway renamed devices attribute", e)); + renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); + }, + e -> log.error("Cannot get gateway renamed devices attribute", e)); + } + } } } @@ -105,17 +131,17 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi if (renamedGatewayDevicesNode.findValue(deletedDeviceName[0]) != null) { renamedGatewayDevicesNode.remove(deletedDeviceName[0]); renamedListChanged.set(true); - } else { - Map renamedGatewayDevicesMap = JacksonUtil.OBJECT_MAPPER.convertValue(renamedGatewayDevicesNode, new TypeReference<>() {}); - renamedGatewayDevicesMap.forEach((key, value) -> { - // If device was renamed earlier - if (deletedDeviceName[0].equals(value)) { - renamedGatewayDevicesNode.remove(key); - deletedDeviceName[0] = key; - renamedListChanged.set(true); - } - }); } + Map renamedGatewayDevicesMap = JacksonUtil.OBJECT_MAPPER.convertValue(renamedGatewayDevicesNode, new TypeReference<>() { + }); + renamedGatewayDevicesMap.forEach((key, value) -> { + // If device was renamed earlier + if (deletedDeviceName[0].equals(value)) { + renamedGatewayDevicesNode.remove(key); + deletedDeviceName[0] = key; + renamedListChanged.set(true); + } + }); if (renamedListChanged.get()) { KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); @@ -138,6 +164,32 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi } } + @Override + public void checkAndUpdateDeletedGatewayDevicesAttribute(Device device) { + List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); + if (!relationToGatewayList.isEmpty()) { + EntityRelation relationToGateway = relationToGatewayList.get(0); + ListenableFuture> deletedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("deletedGatewayDevices")); + DonAsynchron.withCallback(deletedGatewayDevicesFuture, deletedGatewayDevicesList -> { + ArrayNode deletedGatewayDevicesNode; + if (!deletedGatewayDevicesList.isEmpty()) { + int deletedDeviceIndex = -1; + deletedGatewayDevicesNode = (ArrayNode) JacksonUtil.toJsonNode(deletedGatewayDevicesList.get(0).getValueAsString()); + for (int i = 0; i < deletedGatewayDevicesNode.size(); i++) { + if (deletedGatewayDevicesNode.get(i).asText().equals(device.getName())) { + deletedDeviceIndex = i; + } + } + if (deletedDeviceIndex != -1) { + deletedGatewayDevicesNode.remove(deletedDeviceIndex); + KvEntry deletedGatewayDevicesKvEntry = new JsonDataEntry(DELETED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); + saveGatewayDevicesAttribute(device, relationToGateway, deletedGatewayDevicesKvEntry); + } + } + }, e -> log.error("Cannot get gateway deleted devices attribute", e)); + } + } + private void saveGatewayDevicesAttribute(Device device, EntityRelation relationToGateway, KvEntry gatewayDevicesKvEntry) { AttributeKvEntry attrKvEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), gatewayDevicesKvEntry); tsSubService.saveAndNotify(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, List.of(attrKvEntry), true, new FutureCallback() { @@ -152,4 +204,11 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi } }); } + + private boolean isHandleDeviceRenamingEnabled(JsonNode additionalInfo) { + if (additionalInfo.get(HANDLE_DEVICE_RENAMING_PARAMETER) != null) { + return additionalInfo.get(HANDLE_DEVICE_RENAMING_PARAMETER).asBoolean(); + } + return false; + } } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java index f086b25d5a..d4079487a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.service.gateway_device; import org.thingsboard.server.common.data.Device; @@ -7,4 +22,6 @@ public interface GatewayDeviceStateService { void update(Device device, Device oldDevice); void delete(Device device); + + void checkAndUpdateDeletedGatewayDevicesAttribute(Device device); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index cb4e01032f..929d87e5f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -95,6 +95,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; +import org.thingsboard.server.service.gateway_device.GatewayDeviceStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.service.resource.TbResourceService; @@ -137,6 +138,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbResourceService resourceService; private final OtaPackageService otaPackageService; private final OtaPackageDataCache otaPackageDataCache; + private final GatewayDeviceStateService gatewayDeviceStateService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -317,6 +319,8 @@ public class DefaultTransportApiService implements TransportApiService { } relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, lastConnectedGatewayRelation); + gatewayDeviceStateService.checkAndUpdateDeletedGatewayDevicesAttribute(device); + GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() .setDeviceInfo(getDeviceInfoProto(device)); DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index ae0be158d5..cac8ae4c1e 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -103,10 +103,16 @@ {{ 'device.is-gateway' | translate }} +
+
{{ 'device.overwrite-activity-time' | translate }} + + {{ 'device.handle-device-renaming' | translate }} +
device.description diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index fdbc25107f..552e1d5eb8 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -109,6 +109,7 @@ export class DeviceWizardDialogComponent extends label: ['', Validators.maxLength(255)], gateway: [false], overwriteActivityTime: [false], + handleDeviceRenaming: [false], addProfileType: [0], deviceProfileId: [null, Validators.required], newDeviceProfileTitle: [{value: null, disabled: true}], @@ -326,6 +327,7 @@ export class DeviceWizardDialogComponent extends additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value, + handleDeviceRenaming: this.deviceWizardFormGroup.get('handleDeviceRenaming').value, description: this.deviceWizardFormGroup.get('description').value }, customerId: null diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 1187a34f5d..1f89481fd0 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -128,10 +128,16 @@ {{ 'device.is-gateway' | translate }} +
+
{{ 'device.overwrite-activity-time' | translate }} + + {{ 'device.handle-device-renaming' | translate }} +
device.description diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 7cd60a89f4..cd1c78b3dd 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -92,6 +92,7 @@ export class DeviceComponent extends EntityComponent { { gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], overwriteActivityTime: [entity && entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false], + handleDeviceRenaming: [entity && entity.additionalInfo ? entity.additionalInfo.handleDeviceRenaming : false], description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], } ) @@ -121,6 +122,7 @@ export class DeviceComponent extends EntityComponent { additionalInfo: { gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false, overwriteActivityTime: entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false, + handleDeviceRenaming: entity.additionalInfo ? entity.additionalInfo.handleDeviceRenaming : false, description: entity.additionalInfo ? entity.additionalInfo.description : '' } }); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3982bfedda..bfa105092f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1042,6 +1042,7 @@ "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", "is-gateway": "Is gateway", "overwrite-activity-time": "Overwrite activity time for connected device", + "handle-device-renaming": "Handle device renaming", "public": "Public", "device-public": "Device is public", "select-device": "Select device", From 9c44530ab92d8490debf5cc856745b8539f2d656 Mon Sep 17 00:00:00 2001 From: dlandiak Date: Tue, 21 Dec 2021 15:19:30 +0200 Subject: [PATCH 021/403] added boolean param to the mqtt node to allow adding a suffix for client id param --- .../main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java | 3 ++- .../thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 4f4dc7ab60..ac5fa9e7ea 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -104,7 +104,8 @@ public class TbMqttNode implements TbNode { protected MqttClient initClient(TbContext ctx) throws Exception { MqttClientConfig config = new MqttClientConfig(getSslContext()); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { - config.setClientId(this.mqttNodeConfiguration.getClientId()); + config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? + this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index 9ac797a98a..46c81f3ddd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -28,6 +28,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration Date: Wed, 22 Dec 2021 18:12:17 +0200 Subject: [PATCH 022/403] UI: Update and refactoring --- .../server/controller/RpcV2Controller.java | 2 +- ui-ngx/src/app/core/api/widget-api.models.ts | 5 +- .../src/app/core/api/widget-subscription.ts | 41 ------ ui-ngx/src/app/core/http/device.service.ts | 10 +- ui-ngx/src/app/modules/common/modules-map.ts | 2 + .../rpc/persistent-add-dialog.component.html | 2 +- .../rpc/persistent-add-dialog.component.scss | 5 +- .../rpc/persistent-add-dialog.component.ts | 32 ++--- .../persistent-details-dialog.component.html | 2 +- .../persistent-details-dialog.component.scss | 5 +- .../persistent-details-dialog.component.ts | 61 +++------ .../persistent-filter-panel.component.html | 2 +- .../persistent-filter-panel.component.scss | 1 + .../rpc/persistent-filter-panel.component.ts | 16 +-- .../lib/rpc/persistent-table.component.html | 2 +- .../lib/rpc/persistent-table.component.scss | 1 + .../lib/rpc/persistent-table.component.ts | 129 ++++++++++++++---- .../json-object-view.component.html | 2 +- .../components/json-object-view.component.ts | 2 +- ui-ngx/src/app/shared/models/rpc.models.ts | 1 - ui-ngx/src/app/shared/shared.module.ts | 2 +- .../assets/locale/locale.constant-uk_UA.json | 2 +- 22 files changed, 171 insertions(+), 156 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 3f85841b3c..81e9a9e394 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -188,7 +188,7 @@ public class RpcV2Controller extends AbstractRpcController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("DeviceId", strDeviceId); try { - if (rpcStatus.equals(RpcStatus.DELETED)) { + if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED"); } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index c0bab56e4e..7559692561 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -55,8 +55,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; -import { PageLink } from '@shared/models/page/page-link'; -import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; +import { PersistentRpc } from '@shared/models/rpc.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -322,8 +321,6 @@ export interface IWidgetSubscription { persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; clearRpcError(): void; - subscribeForPersistentRequests(pageLink: PageLink, keyFileter: RpcStatus): Observable; - subscribe(): void; subscribeAllForPaginatedData(pageLink: EntityDataPageLink, diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 1634bb7b87..0f864335aa 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -70,7 +70,6 @@ import { import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; -import { PageLink } from '@shared/models/page/page-link'; const moment = moment_; @@ -781,46 +780,6 @@ export class WidgetSubscription implements IWidgetSubscription { } } - subscribeForPersistentRequests(pageLink: PageLink, keyFilter: RpcStatus): Observable { - if (!this.rpcEnabled) { - return throwError(new Error('Rpc disabled!')); - } else if (!this.targetDeviceId) { - return throwError(new Error('Target device is not set!')); - } - const rpcSubject: Subject = new Subject(); - - this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter).subscribe( - (responseBody) => { - rpcSubject.next(responseBody); - rpcSubject.complete(); - }, - (rejection: HttpErrorResponse) => { - const index = this.executingSubjects.indexOf(rpcSubject); - if (index >= 0) { - this.executingSubjects.splice( index, 1 ); - } - this.executingRpcRequest = this.executingSubjects.length > 0; - this.callbacks.rpcStateChanged(this); - if (!this.executingRpcRequest || rejection.status === 504) { - this.rpcRejection = rejection; - if (rejection.status === 504) { - this.rpcErrorText = 'Request Timeout.'; - } else { - this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - const error = this.extractRejectionErrorText(rejection); - if (error) { - this.rpcErrorText += '
'; - this.rpcErrorText += error; - } - } - this.callbacks.onRpcFailed(this); - } - rpcSubject.error(rejection); - } - ); - return rpcSubject.asObservable(); - } - private extractRejectionErrorText(rejection: HttpErrorResponse) { let error = null; if (rejection.error) { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 9b5119d317..88f0cacb46 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -148,10 +148,12 @@ export class DeviceService { } public getPersistedRpcRequests(deviceId: string, pageLink: PageLink, - keyFilter: RpcStatus, config?: RequestConfig): Observable> { - const rpcStatus = keyFilter ? '&rpcStatus=' + keyFilter : ''; - return this.http.get>(`/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}${rpcStatus}`, - defaultHttpOptionsFromConfig(config)); + rpcStatus?: RpcStatus, config?: RequestConfig): Observable> { + let url = `/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}`; + if (rpcStatus && rpcStatus.length) { + url += `&rpcStatus=${rpcStatus}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public findByQuery(query: DeviceSearchQuery, diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 3cd4139ec4..147492ec29 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -139,6 +139,7 @@ import * as QueueTypeListComponent from '@shared/components/queue/queue-type-lis import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component'; import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component'; import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component'; +import * as JsonObjectViewComponent from '@shared/components/json-object-view.component'; import * as JsonContentComponent from '@shared/components/json-content.component'; import * as JsFuncComponent from '@shared/components/js-func.component'; import * as FabToolbarComponent from '@shared/components/fab-toolbar.component'; @@ -420,6 +421,7 @@ class ModulesMap implements IModulesMap { '@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent, '@shared/components/socialshare-panel.component': SocialSharePanelComponent, '@shared/components/json-object-edit.component': JsonObjectEditComponent, + '@shared/components/json-object-view.component': JsonObjectViewComponent, '@shared/components/json-content.component': JsonContentComponent, '@shared/components/js-func.component': JsFuncComponent, '@shared/components/fab-toolbar.component': FabToolbarComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index 7e0ae3056f..1e74657946 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -32,7 +32,7 @@
- {{ 'widgets.persistent-table.message-types.' + persistentFormGroup.get('oneWayElseTwoWay').value | translate }} + {{ rpcMessageTypeText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss index e450fa4a09..3c9d0b43a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - .add-dialog ::ng-deep { + +:host ::ng-deep { + .add-dialog { .params-json-editor, .additional-json-editor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts index c4e87c5e6b..8c4f47a904 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -22,6 +22,7 @@ import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RequestData } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-persistent-add-dialog', @@ -32,15 +33,15 @@ import { RequestData } from '@shared/models/rpc.models'; export class PersistentAddDialogComponent extends DialogComponent implements OnInit { public persistentFormGroup: FormGroup; + public rpcMessageTypeText: string; - private requestData: RequestData = { - persistentUpdated: false - }; + private requestData: RequestData = null; constructor(protected store: Store, protected router: Router, public dialogRef: MatDialogRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); this.persistentFormGroup = this.fb.group( @@ -48,27 +49,24 @@ export class PersistentAddDialogComponent extends DialogComponent { + this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`); + } + ); } close(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html index 3a9451db67..440b142db2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -48,7 +48,7 @@ widgets.persistent-table.status + [ngStyle]="{ fontWeight: 'bold', color: rpcStatusColorsMap.get(data.persistentRequest.status) }"> widgets.persistent-table.method diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss index ec456d5d9e..2a6a06c2a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - .rpc-dialog ::ng-deep { +:host ::ng-deep { + .rpc-dialog { .mat-expansion-panel-body { padding-bottom: 0 !important; } @@ -23,6 +23,7 @@ margin: 0 0 16px 0; } } + .tb-audit-log-response-data { width: 100%; min-width: 400px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts index 552175b905..8e7e755e86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -24,13 +24,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup } from '@angular/forms'; import { DeviceService } from '@core/http/device.service'; -import { - PersistentRpc, - rpcStatusColors, - RpcStatus, - rpcStatusTranslation -} from '@shared/models/rpc.models'; -import { isDefinedAndNotNull } from '@core/utils'; +import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DialogService } from '@core/services/dialog.service'; @@ -82,7 +76,7 @@ export class PersistentDetailsDialogComponent extends DialogComponent { if (res) { - if (res) { - this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { - this.persistentUpdated = true; - this.close(); - }); - } + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.persistentUpdated = true; + this.close(); + }); } }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html index ffbcab5749..d5eca15a5f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html @@ -19,7 +19,7 @@ widgets.persistent-table.rpc-status-list + placeholder="{{ rpcSearchPlaceholder }}"> {{ 'widgets.persistent-table.rpc-search-status-all' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss index 63f832d78c..2ad57a3f7f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + :host { width: 100%; height: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts index d60c80ac43..c8a72ffc3b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -18,6 +18,7 @@ import { Component, Inject, InjectionToken } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { OverlayRef } from '@angular/cdk/overlay'; import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); @@ -35,26 +36,21 @@ export class PersistentFilterPanelComponent { public persistentFilterFormGroup: FormGroup; public result: PersistentFilterPanelData; public rpcSearchStatusTranslationMap = rpcStatusTranslation; + public rpcSearchPlaceholder: string; - public persistentSearchStatuses = [ - RpcStatus.QUEUED, - RpcStatus.SENT, - RpcStatus.DELIVERED, - RpcStatus.SUCCESSFUL, - RpcStatus.TIMEOUT, - RpcStatus.EXPIRED, - RpcStatus.FAILED - ]; + public persistentSearchStatuses = Object.keys(RpcStatus); constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) public data: PersistentFilterPanelData, public overlayRef: OverlayRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { this.persistentFilterFormGroup = this.fb.group( { rpcStatus: this.data.rpcStatus } ); + this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status'); } update() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html index fd86fd92cc..e786efec7b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
{ - const showHidePageSize = this.persistentWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.persistentWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } @@ -250,7 +257,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.displayedColumns.push('actions'); } - this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription); + this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx); const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); @@ -300,11 +307,9 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.translate.instant('action.yes') ).subscribe((res) => { if (res) { - if (res) { - this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { - this.reloadPersistentRequests(); - }); - } + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.reloadPersistentRequests(); + }); } }); } @@ -345,7 +350,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] }).afterClosed().subscribe( (requestData) => { - if (requestData.persistentUpdated) { + if (requestData) { this.sendRequests(requestData); } } @@ -440,11 +445,16 @@ class PersistentDatasource implements DataSource { private persistentSubject = new BehaviorSubject([]); private pageDataSubject = new BehaviorSubject>(emptyPageData()); + private rpcErrorText: string; + private executingSubjects: Array>; + private executingRpcRequest = false; + public dataLoading = true; public pageData$ = this.pageDataSubject.asObservable(); constructor(private translate: TranslateService, - private subscription: IWidgetSubscription) { + private subscription: IWidgetSubscription, + private ctx: WidgetContext) { } connect(collectionViewer: CollectionViewer): Observable> { @@ -462,11 +472,11 @@ class PersistentDatasource implements DataSource { this.pageDataSubject.next(pageData); } - loadPersistent(pageLink: PageLink, keyFilter: RpcStatus) { + loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) { this.dataLoading = true; const result = new ReplaySubject>(); - this.fetchEntities(pageLink, keyFilter).pipe( + this.fetchEntities(pageLink, rpcStatusFilter).pipe( catchError(() => of(emptyPageData())), ).subscribe( (pageData) => { @@ -479,8 +489,81 @@ class PersistentDatasource implements DataSource { return result; } - fetchEntities(pageLink: PageLink, keyFilter: RpcStatus): Observable> { - return this.subscription.subscribeForPersistentRequests(pageLink, keyFilter); + fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable> { + if (!this.subscription.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else if (!this.subscription.targetDeviceId) { + return throwError(new Error('Target device is not set!')); + } + const rpcSubject: Subject = new Subject(); + + this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe( + (responseBody) => { + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + this.rpcErrorText = null; + this.executingSubjects = []; + + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice(index, 1); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.subscription.options.callbacks.rpcStateChanged(this.subscription); + if (!this.executingRpcRequest || rejection.status === 504) { + this.subscription.rpcRejection = rejection; + if (rejection.status === 504) { + this.subscription.rpcErrorText = 'Request Timeout.'; + } else { + this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.subscription.rpcErrorText += '
'; + this.subscription.rpcErrorText += error.message || ''; + } + } + this.subscription.callbacks.onRpcFailed(this.subscription); + } + rpcSubject.error(rejection); + } + ); + return rpcSubject.asObservable(); + } + + extractRejectionErrorText(rejection: HttpErrorResponse) { + let error = null; + if (rejection.error) { + error = rejection.error; + try { + error = rejection.error ? JSON.parse(rejection.error) : null; + } catch (e) {} + } + if (error && !error.message) { + error = this.prepareMessageFromData(error); + } else if (error && error.message) { + error = error.message; + } + return error; + } + + prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } } isEmpty(): Observable { diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.html b/ui-ngx/src/app/shared/components/json-object-view.component.html index 379c557b44..b3fbcef2de 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.html +++ b/ui-ngx/src/app/shared/components/json-object-view.component.html @@ -18,5 +18,5 @@
-
+
diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts index 5ce00b0189..b85988c3d3 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -153,7 +153,7 @@ export class JsonObjectViewComponent implements OnInit { }, 2); } } catch (e) { - // + console.error(e); } if (this.jsonViewer) { this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); diff --git a/ui-ngx/src/app/shared/models/rpc.models.ts b/ui-ngx/src/app/shared/models/rpc.models.ts index 61a94a401e..4512b590a0 100644 --- a/ui-ngx/src/app/shared/models/rpc.models.ts +++ b/ui-ngx/src/app/shared/models/rpc.models.ts @@ -79,7 +79,6 @@ export interface PersistentRpcData extends PersistentRpc { } export interface RequestData { - persistentUpdated: boolean; method?: string; oneWayElseTwoWay?: boolean; persistentPollingInterval?: number; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 71a4d344bf..bb16964806 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -150,7 +150,7 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo import { HelpPopupComponent } from '@shared/components/help-popup.component'; import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component'; import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; -import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; +import { TbComponentOutletDirective } from '@shared/components/directives/component-outlet.directive'; import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; import { MarkedOptionsService } from '@shared/components/marked-options.service'; import { TbPopoverService } from '@shared/components/popover.service'; diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 3942a5193c..943598baef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2395,7 +2395,7 @@ }, "rpc-search-status-all": "ВСІ", "message-types": { - "false": "Двусторонній", + "false": "Двосторонній", "true": "Односторонній" } } From c20f6e4021a29bc0f3f6a38ac19392cbbdf21258 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 23 Dec 2021 11:20:22 +0200 Subject: [PATCH 023/403] UI: Add in map widgets new group setting - Editor setting --- ui-ngx/package.json | 2 +- .../components/widget/lib/maps/leaflet-map.ts | 52 +++++---- .../components/widget/lib/maps/map-models.ts | 19 +++- .../components/widget/lib/maps/map-widget2.ts | 3 + .../components/widget/lib/maps/markers.ts | 29 ++--- .../components/widget/lib/maps/polygon.ts | 7 +- .../components/widget/lib/maps/schemes.ts | 58 ++++++++++ ui-ngx/yarn.lock | 107 ++++++++---------- 8 files changed, 178 insertions(+), 99 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index adec5e815a..1153524a26 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -28,7 +28,7 @@ "@date-io/date-fns": "^2.11.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "~0.4.6", - "@geoman-io/leaflet-geoman-free": "^2.11.3", + "@geoman-io/leaflet-geoman-free": "^2.11.4", "@juggle/resize-observer": "^3.3.1", "@mat-datetimepicker/core": "~7.0.1", "@material-ui/core": "^4.12.3", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 78f459990e..9d661ae8d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -24,7 +24,9 @@ import { defaultSettings, FormattedData, MapSettings, - MarkerSettings, MarkerIconInfo, MarkerImageInfo, + MarkerIconInfo, + MarkerImageInfo, + MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo, @@ -182,7 +184,7 @@ export default abstract class LeafletMap { addEditControl() { // Customize edit marker - if (this.options.draggableMarker) { + if (this.options.draggableMarker && !this.options.hideDrawControlButton) { const actions = [{ text: L.PM.Utils.getTranslation('actions.cancel'), onClick: () => this.toggleDrawMode('tbMarker') @@ -197,7 +199,7 @@ export default abstract class LeafletMap { } // Customize edit polygon - if (this.editPolygons) { + if (this.editPolygons && !this.options.hideDrawControlButton) { const rectangleActions = [ { text: L.PM.Utils.getTranslation('actions.cancel'), @@ -231,18 +233,27 @@ export default abstract class LeafletMap { const translateService = this.ctx.$injector.get(TranslateService); this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en'); - this.map.pm.addControls({ - position: 'topleft', - drawMarker: false, - drawCircle: false, - drawCircleMarker: false, - drawRectangle: false, - drawPolyline: false, - drawPolygon: false, - editMode: this.editPolygons, - cutPolygon: this.editPolygons, - rotateMode: this.editPolygons - }); + if (!this.options.hideAllControlButton) { + this.map.pm.addControls({ + position: 'topleft', + drawControls: !this.options.hideDrawControlButton, + drawMarker: false, + drawCircle: false, + drawCircleMarker: false, + drawRectangle: false, + drawPolyline: false, + drawPolygon: false, + dragMode: !this.options.hideEditControlButton, + editMode: this.editPolygons && !this.options.hideEditControlButton, + cutPolygon: this.editPolygons && !this.options.hideEditControlButton, + removalMode: !this.options.hideRemoveControlButton, + rotateMode: this.editPolygons && !this.options.hideEditControlButton + }); + } + + if (this.options.initDragMode) { + this.map.pm.enableGlobalDragMode(); + } this.map.on('pm:create', (e) => { if (e.shape === 'tbMarker') { @@ -344,6 +355,7 @@ export default abstract class LeafletMap { } if (this.options.draggableMarker || this.editPolygons) { map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + map.pm.applyGlobalOptions(); this.addEditControl(); } else { this.map.pm.disableDraw(); @@ -555,7 +567,7 @@ export default abstract class LeafletMap { updatedMarkers.push(m); } } else { - m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); + m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback); if (m) { createdMarkers.push(m); } @@ -569,7 +581,7 @@ export default abstract class LeafletMap { } }); this.markersData = markersData; - if ((this.options as MarkerSettings).useClusterMarkers) { + if (this.options.useClusterMarkers) { if (createdMarkers.length) { this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); } @@ -589,7 +601,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); } - private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true, callback?): Marker { const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); if (callback) { @@ -597,7 +609,7 @@ export default abstract class LeafletMap { callback(data, true); }); } - if (this.bounds && updateBounds && !(this.options as MarkerSettings).useClusterMarkers) { + if (this.bounds && updateBounds && !this.options.useClusterMarkers) { this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); } this.markers.set(key, newMarker); @@ -786,7 +798,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } - createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true) { const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); if (updateBounds) { const bounds = polygon.leafletPoly.getBounds(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index e65479f588..de94bdc156 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -192,6 +192,15 @@ export type PolylineSettings = { strokeWeightFunction: GenericFunction; }; +export interface EditorSettings { + snappable: boolean; + initDragMode: boolean; + hideAllControlButton: boolean; + hideDrawControlButton: boolean; + hideEditControlButton: boolean; + hideRemoveControlButton: boolean; +} + export interface HistorySelectSettings { buttonColor: string; } @@ -231,7 +240,7 @@ export interface TripAnimationSettings extends PolygonSettings { export type actionsHandler = ($event: Event, datasource: Datasource) => void; -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings; +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings & EditorSettings; export const defaultSettings: any = { xPosKeyName: 'xPos', @@ -271,7 +280,13 @@ export const defaultSettings: any = { draggableMarker: false, editablePolygon: false, fitMapBounds: true, - mapPageSize: DEFAULT_MAP_PAGE_SIZE + mapPageSize: DEFAULT_MAP_PAGE_SIZE, + snappable: false, + initDragMode: false, + hideAllControlButton: false, + hideDrawControlButton: false, + hideEditControlButton: false, + hideRemoveControlButton: false }; export const hereProviders = [ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 565887b160..bc042e4fc0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -18,6 +18,7 @@ import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapS import LeafletMap from './leaflet-map'; import { commonMapSettingsSchema, + editorSettingSchema, mapPolygonSchema, markerClusteringSettingsSchema, markerClusteringSettingsSchemaLeaflet, @@ -115,6 +116,8 @@ export class MapWidgetController implements MapWidgetInterface { `model.useClusterMarkers === true && model.provider !== "image-map"`)]); addToSchema(schema, clusteringSchema); addGroupInfo(schema, 'Markers Clustering Settings'); + addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)')); + addGroupInfo(schema, 'Editor settings'); } return schema; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 85020b7f33..741cf22719 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -14,19 +14,17 @@ /// limitations under the License. /// -import L, { Icon, LeafletMouseEvent } from 'leaflet'; -import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models'; +import L, { LeafletMouseEvent } from 'leaflet'; import { - bindPopupActions, - createTooltip, -} from './maps-utils'; -import { - aspectCache, - fillPattern, - parseWithTranslation, - processPattern, - safeExecute -} from './common-maps-utils'; + FormattedData, + MarkerIconInfo, + MarkerIconReadyFunction, + MarkerImageInfo, + MarkerSettings, + UnitedMapSettings +} from './map-models'; +import { bindPopupActions, createTooltip, } from './maps-utils'; +import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils'; import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import LeafletMap from './leaflet-map'; @@ -40,10 +38,13 @@ export class Marker { data: FormattedData; dataSources: FormattedData[]; - constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, + constructor(private map: LeafletMap, private location: L.LatLng, public settings: UnitedMapSettings, data?: FormattedData, dataSources?, onDragendListener?) { this.setDataSources(data, dataSources); - this.leafletMarker = L.marker(location, {pmIgnore: !settings.draggableMarker}); + this.leafletMarker = L.marker(location, { + pmIgnore: !settings.draggableMarker, + snapIgnore: !settings.snappable + }); this.markerOffset = [ isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index 908952e7d2..de55d9b1b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -23,7 +23,7 @@ import { processPattern, safeExecute } from './common-maps-utils'; -import { FormattedData, MarkerSettings, PolygonSettings } from './map-models'; +import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models'; export class Polygon { @@ -32,7 +32,7 @@ export class Polygon { data: FormattedData; dataSources: FormattedData[]; - constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, + constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: UnitedMapSettings, private onDragendListener?) { this.dataSources = dataSources; this.data = data; @@ -47,7 +47,8 @@ export class Polygon { weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, - pmIgnore: !settings.editablePolygon + pmIgnore: !settings.editablePolygon, + snapIgnore: !settings.snappable }).addTo(this.map); this.updateLabel(settings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 2924ff688e..1c383b75e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -1311,3 +1311,61 @@ export const providerSets: { [key: string]: IProvider } = { name: 'image-map' } }; + +export const editorSettingSchema = + { + schema: { + title: 'Editor settings', + type: 'object', + properties: { + snappable: { + title: 'Enable snapping to other vertices for precision drawing', + type: 'boolean', + default: false + }, + initDragMode: { + title: 'Initialize map in draggable mode', + type: 'boolean', + default: false + }, + hideAllControlButton: { + title: 'Hide all button', + type: 'boolean', + default: false + }, + hideDrawControlButton: { + title: 'Hide draw buttons', + type: 'boolean', + default: false + }, + hideEditControlButton: { + title: 'Hide edit buttons', + type: 'boolean', + default: false + }, + hideRemoveControlButton: { + title: 'Hide remove button', + type: 'boolean', + default: false + }, + }, + required: [] + }, + form: [ + 'snappable', + 'initDragMode', + 'hideAllControlButton', + { + key: 'hideDrawControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideEditControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideRemoveControlButton', + condition: 'model.hideAllControlButton == false' + } + ] + }; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index f1e5a9abc9..25ca2b4b64 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1308,15 +1308,15 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@geoman-io/leaflet-geoman-free@^2.11.3": - version "2.11.3" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.3.tgz#480164ab76c2b2a885003e0c111284f3c3160a36" - integrity sha512-LsiurEgKEHBcTnAVl8h7EfS5V/doCuxePzPE9SnfrhtJBN7IzP6UwkEo35Agwko+BnIuw/o2bE4F7irvKwQzjw== - dependencies: - "@turf/boolean-contains" "6.3.0" - "@turf/kinks" "6.3.0" - "@turf/line-intersect" "6.3.0" - "@turf/line-split" "6.3.0" +"@geoman-io/leaflet-geoman-free@^2.11.4": + version "2.11.4" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.4.tgz#4a43fa8d3d5d2bca751135b775c19c6cc0063699" + integrity sha512-uWfgaGDhrtoCMHdHi2oNVKb8WXFMQvyNnan1sS/+Yn5jMPuhijWFyAjy0G5kTCamXhGXg4vUvlEpiRSrBwewKg== + dependencies: + "@turf/boolean-contains" "^6.5.0" + "@turf/kinks" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-split" "^6.5.0" lodash "4.17.21" polygon-clipping "0.15.3" @@ -1601,7 +1601,7 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@turf/bbox@*", "@turf/bbox@^6.3.0": +"@turf/bbox@*", "@turf/bbox@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" integrity sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw== @@ -1617,18 +1617,18 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/boolean-contains@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.3.0.tgz#fe4fc359e408c8c3c89e7fb159c9d31fde48779a" - integrity sha512-1MW7B5G5tIu1lnAv3pXyFzl75wfBYnbA2GhwHDb4okIXMhloy/r5uIqAZHo0fOXykKVJS/gIfA/MioKIftoTug== +"@turf/boolean-contains@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz#f802e7432fb53109242d5bf57393ef2f53849bbf" + integrity sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ== dependencies: - "@turf/bbox" "^6.3.0" - "@turf/boolean-point-in-polygon" "^6.3.0" - "@turf/boolean-point-on-line" "^6.3.0" - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" + "@turf/bbox" "^6.5.0" + "@turf/boolean-point-in-polygon" "^6.5.0" + "@turf/boolean-point-on-line" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" -"@turf/boolean-point-in-polygon@^6.3.0": +"@turf/boolean-point-in-polygon@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz#6d2e9c89de4cd2e4365004c1e51490b7795a63cf" integrity sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A== @@ -1636,7 +1636,7 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/boolean-point-on-line@^6.3.0": +"@turf/boolean-point-on-line@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz#a8efa7bad88760676f395afb9980746bc5b376e9" integrity sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ== @@ -1660,37 +1660,26 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/helpers@6.x", "@turf/helpers@^6.3.0", "@turf/helpers@^6.5.0": +"@turf/helpers@6.x", "@turf/helpers@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== -"@turf/invariant@^6.3.0", "@turf/invariant@^6.5.0": +"@turf/invariant@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== dependencies: "@turf/helpers" "^6.5.0" -"@turf/kinks@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.3.0.tgz#a16b4ccc5a5aae139d43e36271e0a0494fdb4bf7" - integrity sha512-BLWvbl2/fa4SeJzVMbleT6Vo1cmzwmzRfxL2xxMei2jmf6JSvqDoMJFwIHGXrLZXvhOCb1b2C+MhBfhtc7kYkQ== - dependencies: - "@turf/helpers" "^6.3.0" - -"@turf/line-intersect@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.3.0.tgz#726a50edc66bb7b5e798b052b103fb0da4d1c4f4" - integrity sha512-3naxR7XpkPd2vst3Mw6DFry4C9m3o0/f2n/xu5UAyxb88Ie4m2k+1eqkhzMMx/0L+E6iThWpLx7DASM6q6o9ow== +"@turf/kinks@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.5.0.tgz#80e7456367535365012f658cf1a988b39a2c920b" + integrity sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ== dependencies: - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" - "@turf/line-segment" "^6.3.0" - "@turf/meta" "^6.3.0" - geojson-rbush "3.x" + "@turf/helpers" "^6.5.0" -"@turf/line-intersect@^6.3.0", "@turf/line-intersect@^6.5.0": +"@turf/line-intersect@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020" integrity sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA== @@ -1701,7 +1690,7 @@ "@turf/meta" "^6.5.0" geojson-rbush "3.x" -"@turf/line-segment@^6.3.0", "@turf/line-segment@^6.5.0": +"@turf/line-segment@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-6.5.0.tgz#ee73f3ffcb7c956203b64ed966d96af380a4dd65" integrity sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw== @@ -1710,30 +1699,30 @@ "@turf/invariant" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/line-split@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.3.0.tgz#ee218f66cd65ce84eafc4956c24083663f6082ea" - integrity sha512-Q0nUJ0vczy11piyEz0FaKScFwSQtb1HJ2RPEMCw1coUJhTCB02KBWQLImhYqwsD3uLg+H/fxaJ1Gva6EPWoDNQ== - dependencies: - "@turf/bbox" "^6.3.0" - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" - "@turf/line-intersect" "^6.3.0" - "@turf/line-segment" "^6.3.0" - "@turf/meta" "^6.3.0" - "@turf/nearest-point-on-line" "^6.3.0" - "@turf/square" "^6.3.0" - "@turf/truncate" "^6.3.0" +"@turf/line-split@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.5.0.tgz#116d7fbf714457878225187f5820ef98db7b02c2" + integrity sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw== + dependencies: + "@turf/bbox" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-segment" "^6.5.0" + "@turf/meta" "^6.5.0" + "@turf/nearest-point-on-line" "^6.5.0" + "@turf/square" "^6.5.0" + "@turf/truncate" "^6.5.0" geojson-rbush "3.x" -"@turf/meta@6.x", "@turf/meta@^6.3.0", "@turf/meta@^6.5.0": +"@turf/meta@6.x", "@turf/meta@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" integrity sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA== dependencies: "@turf/helpers" "^6.5.0" -"@turf/nearest-point-on-line@^6.3.0": +"@turf/nearest-point-on-line@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz#8e1cd2cdc0b5acaf4c8d8b3b33bb008d3cb99e7b" integrity sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg== @@ -1746,7 +1735,7 @@ "@turf/line-intersect" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/square@^6.3.0": +"@turf/square@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/square/-/square-6.5.0.tgz#ab43eef99d39c36157ab5b80416bbeba1f6b2122" integrity sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ== @@ -1754,7 +1743,7 @@ "@turf/distance" "^6.5.0" "@turf/helpers" "^6.5.0" -"@turf/truncate@^6.3.0": +"@turf/truncate@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-6.5.0.tgz#c3a16cad959f1be1c5156157d5555c64b19185d8" integrity sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg== From d7f4bd5436c72b1701fd806d9661bbd1ce203371 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 23 Dec 2021 14:16:22 +0200 Subject: [PATCH 024/403] UI: Show legend fieldset only in timeseries and latest widget --- .../home/components/widget/legend.component.ts | 12 +++++++----- .../components/widget/widget-config.component.html | 2 +- .../components/widget/widget-config.component.ts | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts index 004fd5087b..3f66fc8aba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts @@ -60,11 +60,13 @@ export class LegendComponent implements OnInit { } legendKeys(): LegendKey[] { - let keys = this.legendData.keys; - if (this.legendConfig.sortDataKeys) { - keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); - } - return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + try { + let keys = this.legendData.keys; + if (this.legendConfig.sortDataKeys) { + keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); + } + return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + } catch (e) {} } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 97542fd3cb..6878a19d60 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -421,7 +421,7 @@ -
+
widget-config.legend diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 7d779a3a6c..e64cc4de42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -159,6 +159,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont modelValue: WidgetConfigComponentData; + showLegendFieldset = true; + private propagateChange = null; public dataSettings: FormGroup; @@ -322,6 +324,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.datasourceTypes.push(DatasourceType.entityCount); } } + this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); + this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); From 94d502e6eedde28c9360789c43cfda454efd2b4b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 11:13:28 +0200 Subject: [PATCH 025/403] UI: Refactoring --- .../modules/home/components/widget/widget-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index e64cc4de42..6b485dd66b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -324,7 +324,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.datasourceTypes.push(DatasourceType.entityCount); } } - this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); @@ -389,6 +388,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (this.modelValue) { if (this.widgetType !== this.modelValue.widgetType) { this.widgetType = this.modelValue.widgetType; + this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); this.buildForms(); } const config = this.modelValue.config; From cfead9941ed1a66e29b89adaf28cad40ca28b982 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 24 Dec 2021 11:28:20 +0200 Subject: [PATCH 026/403] UI: Fixed code style --- ui-ngx/src/app/shared/components/json-object-view.component.ts | 1 - ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts index b85988c3d3..483d1a014a 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -127,7 +127,6 @@ export class JsonObjectViewComponent implements OnInit { newWidth = 8 * maxLineLength + 16; } if (this.heigthValue) { - // this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); } if (this.widthValue) { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index bd33c10376..e4fa66b4e5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3279,7 +3279,7 @@ "EXPIRED": "EXPIRED", "FAILED": "FAILED" }, - "rpc-search-status-all": "ALL", + "rpc-search-status-all": "ALL", "message-types": { "false": "Two-way", "true": "One-way" From 8a421e3aa0cf399573940437f98116f0b9ebd271 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 16:05:24 +0200 Subject: [PATCH 027/403] UI: Fixed constant boolean filters creating form --- .../filter/boolean-filter-predicate.component.ts | 7 +++++-- .../home/components/filter/key-filter-dialog.component.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index d5299a52ea..e0a4868898 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -109,8 +109,11 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, Va } private updateModel() { - const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.BOOLEAN; + let predicate: BooleanFilterPredicate = null; + if (this.booleanFilterPredicateFormGroup.valid) { + predicate = this.booleanFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.BOOLEAN; + } this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index 6c9c85330a..41520eede7 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -134,6 +134,10 @@ export class KeyFilterDialogComponent extends } ); } + if (valueType === EntityKeyValueType.BOOLEAN) { + this.keyFilterFormGroup.get('value').clearValidators(); + this.keyFilterFormGroup.get('value').updateValueAndValidity(); + } }); this.keyFilterFormGroup.get('key.type').valueChanges.pipe( @@ -148,7 +152,7 @@ export class KeyFilterDialogComponent extends this.showAutocomplete = false; } if (this.data.telemetryKeysOnly) { - if (type === EntityKeyType.CONSTANT) { + if (type === EntityKeyType.CONSTANT && (this.keyFilterFormGroup.get('valueType').value !== EntityKeyValueType.BOOLEAN)) { this.keyFilterFormGroup.get('value').setValidators(Validators.required); this.keyFilterFormGroup.get('value').updateValueAndValidity(); } else { From fef7b2605066cb39322de5e9b941c8b39140067b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 17:03:51 +0200 Subject: [PATCH 028/403] UI: Added a condition for ckeck boolean key type only for constant type --- .../home/components/filter/key-filter-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index 41520eede7..c96bc66419 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -134,7 +134,7 @@ export class KeyFilterDialogComponent extends } ); } - if (valueType === EntityKeyValueType.BOOLEAN) { + if (valueType === EntityKeyValueType.BOOLEAN && this.isConstantKeyType) { this.keyFilterFormGroup.get('value').clearValidators(); this.keyFilterFormGroup.get('value').updateValueAndValidity(); } From edcde714c7fbb1255c9e628317e30cf598b57744 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 28 Dec 2021 12:01:26 +0200 Subject: [PATCH 029/403] UI: Added entity details page --- ui-ngx/src/app/core/utils.ts | 14 ++ .../entity/entity-details-page.component.html | 57 ++++++ .../entity/entity-details-page.component.scss | 119 +++++++++++++ .../entity/entity-details-page.component.ts | 154 ++++++++++++++++ .../entity/entity-details-panel.component.ts | 16 +- .../components/entity/entity.component.ts | 2 + .../home/components/home-components.module.ts | 3 + ...device-profile-autocomplete.component.html | 7 + ...device-profile-autocomplete.component.scss | 21 +++ .../device-profile-autocomplete.component.ts | 9 +- ...ofile-provision-configuration.component.ts | 2 +- .../profile/device-profile.component.html | 8 + ...tenant-profile-autocomplete.component.html | 7 + ...tenant-profile-autocomplete.component.scss | 21 +++ .../tenant-profile-autocomplete.component.ts | 16 +- .../profile/tenant-profile.component.html | 6 + .../relation/relation-table.component.html | 8 +- .../relation/relation-table.component.scss | 3 + .../models/datasource/relation-datasource.ts | 5 +- .../home/pages/admin/admin-routing.module.ts | 38 +++- .../resources-library-table-config.resolve.ts | 13 +- .../resource/resources-library.component.html | 6 + .../resource/resources-library.component.ts | 5 +- .../home/pages/asset/asset-routing.module.ts | 42 ++++- .../home/pages/asset/asset.component.html | 6 + .../asset/assets-table-config.resolver.ts | 10 ++ .../pages/customer/customer-routing.module.ts | 167 +++++++++++++++--- .../pages/customer/customer.component.html | 6 + .../customers-table-config.resolver.ts | 10 ++ .../device-profile-routing.module.ts | 20 +++ .../device-profiles-table-config.resolver.ts | 13 +- .../pages/device/device-routing.module.ts | 42 ++++- .../home/pages/device/device.component.html | 7 + .../home/pages/device/device.component.ts | 1 + .../device/devices-table-config.resolver.ts | 10 ++ .../home/pages/edge/edge-routing.module.ts | 131 ++++++++++++-- .../home/pages/edge/edge.component.html | 6 + .../pages/edge/edges-table-config.resolver.ts | 10 ++ .../entity-view/entity-view-routing.module.ts | 42 ++++- .../entity-view/entity-view.component.html | 6 + .../entity-views-table-config.resolver.ts | 10 ++ .../ota-update/ota-update-routing.module.ts | 39 +++- .../ota-update-table-config.resolve.ts | 13 +- .../ota-update/ota-update.component.html | 9 +- .../tenant-profile-routing.module.ts | 20 +++ .../tenant-profiles-table-config.resolver.ts | 15 +- .../pages/tenant/tenant-routing.module.ts | 55 +++++- .../home/pages/tenant/tenant.component.html | 7 + .../tenant/tenants-table-config.resolver.ts | 10 ++ .../home/pages/user/user-routing.module.ts | 43 ++++- .../home/pages/user/user.component.html | 6 + .../pages/user/users-table-config.resolver.ts | 27 ++- .../shared/components/breadcrumb.component.ts | 8 +- .../ota-package-autocomplete.component.html | 7 + .../ota-package-autocomplete.component.scss | 21 +++ .../ota-package-autocomplete.component.ts | 10 +- ui-ngx/src/app/shared/models/constants.ts | 15 ++ .../src/app/shared/models/relation.models.ts | 1 + ui-ngx/src/app/shared/shared.module.ts | 1 + .../assets/locale/locale.constant-en_US.json | 3 +- 60 files changed, 1271 insertions(+), 118 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.scss create mode 100644 ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b986583171..c09488a593 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -20,6 +20,10 @@ import { finalize, share } from 'rxjs/operators'; import { Datasource } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { baseDetailsPageByEntityType } from '@shared/models/constants'; +import { EntityType } from '@shared/models/entity-type.models'; const varsRegex = /\${([^}]*)}/g; @@ -460,3 +464,13 @@ export function randomAlphanumeric(length: number): string { } return result; } + +export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => { + return component.entity?.name || component.headerSubtitle; +}); + + +export function getEntityDetailsPageURL(id: string, entityType: EntityType): string { + return `${baseDetailsPageByEntityType.get(entityType)}/${id}`; +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html new file mode 100644 index 0000000000..e025534f2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html @@ -0,0 +1,57 @@ + + + +
+
+
{{ headerTitle }}
+
{{ headerSubtitle }}
+
+
+ +
+ + +
+
+
+ + + + + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss new file mode 100644 index 0000000000..a74e9e46c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../../../scss/constants"; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + + .settings-card { + margin: 8px; + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + .details-toolbar { + height: 84px; + min-height: 84px; + border-radius: 4px 4px 0 0; + background: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + + .mat-toolbar-tools { + padding: 0 8px; + } + + .tb-details-title-header { + min-width: 0; + width: auto; + } + + .tb-details-title { + font-size: 1rem; + font-weight: 500; + + @media #{$mat-gt-sm} { + font-size: 1.2rem; + } + } + + .tb-details-subtitle { + font-size: 0.9rem; + opacity: .8; + } + + .tb-ellipsis { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + @media #{$mat-md} { + width: 80%; + } + + @media #{$mat-gt-md} { + width: 60%; + } + + .tb-header-button { + .tb-btn-header { + position: relative !important; + display: inline-block !important; + animation: tbMoveFromTopFade .3s ease both; + + &.tb-hide { + animation: tbMoveToTopFade .3s ease both; + } + } + } + } +} + +:host ::ng-deep { + .tb-help { + .mat-icon-button.mat-primary { + color: rgba(0, 0, 0, 0.52); + } + } + + .mat-card-content { + position: relative; + overflow: hidden; + + > .mat-tab-group { + > .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + > .mat-tab-header { + .mat-tab-label { + min-width: 40px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts new file mode 100644 index 0000000000..27ed899c79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + HostBinding, + Injector, + OnDestroy, + OnInit +} from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { AssetId } from '@shared/models/id/asset-id'; +import { TranslateService } from '@ngx-translate/core'; +import { deepClone, mergeDeep } from '@core/utils'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; + +@Component({ + selector: 'tb-entity-details-page', + templateUrl: './entity-details-page.component.html', + styleUrls: ['./entity-details-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements OnInit, OnDestroy { + + headerTitle: string; + headerSubtitle: string; + + isReadOnly = false; + + set entitiesTableConfig(entitiesTableConfig: EntityTableConfig>) { + if (this.entitiesTableConfigValue !== entitiesTableConfig) { + this.entitiesTableConfigValue = entitiesTableConfig; + if (this.entitiesTableConfigValue) { + this.isEdit = false; + this.entity = null; + } + } + } + + get entitiesTableConfig(): EntityTableConfig> { + return this.entitiesTableConfigValue; + } + + @HostBinding('class') 'tb-absolute-fill'; + + constructor(private route: ActivatedRoute, + private router: Router, + protected injector: Injector, + protected cd: ChangeDetectorRef, + protected componentFactoryResolver: ComponentFactoryResolver, + private broadcast: BroadcastService, + private translate: TranslateService, + protected store: Store) { + super(store, injector, cd, componentFactoryResolver); + this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; + } + + ngOnInit() { + this.headerSubtitle = ''; + this.route.paramMap.subscribe( paramMap => { + this.entityId = new AssetId(paramMap.get('entityId')); + }); + this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); + super.init(); + this.entityComponent.isDetailsPage = true; + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + reload(): void { + this.isEdit = false; + this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( + (entity) => { + this.entity = entity; + this.broadcast.broadcast('updateBreadcrumb'); + this.isReadOnly = this.entitiesTableConfig.detailsReadonly(entity); + this.headerTitle = this.entitiesTableConfig.entityTitle(entity); + this.entityComponent.entity = entity; + this.entityComponent.isEdit = false; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + } + ); + } + + onToggleDetailsEditMode() { + if (this.isEdit) { + this.entityComponent.entity = this.entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.entity; + } + this.isEdit = !this.isEdit; + } else { + this.isEdit = !this.isEdit; + this.editingEntity = deepClone(this.entity); + this.entityComponent.entity = this.editingEntity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.editingEntity; + } + if (this.entitiesTableConfig.hideDetailsTabsOnEdit) { + this.selectedTab = 0; + } + } + } + + onApplyDetails() { + if (this.detailsForm && this.detailsForm.valid) { + const editingEntity = {...this.editingEntity, ...this.detailsForm.getRawValue()}; + if (this.detailsForm.hasOwnProperty('additionalInfo')) { + editingEntity.additionalInfo = + mergeDeep((this.editingEntity as any).additionalInfo, this.detailsForm.getRawValue()?.additionalInfo); + } + this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( + (entity) => { + this.entity = entity; + this.entityComponent.entity = entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + this.isEdit = false; + } + ); + } + } + + confirmForm(): FormGroup { + return this.detailsForm; + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index d6378620f9..c5e7968935 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -88,15 +88,15 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV entity: BaseData; editingEntity: BaseData; - private currentEntityId: HasId; - private subscriptions: Subscription[] = []; - private viewInited = false; - private pendingTabs: MatTab[]; + protected currentEntityId: HasId; + protected subscriptions: Subscription[] = []; + protected viewInited = false; + protected pendingTabs: MatTab[]; constructor(protected store: Store, - private injector: Injector, - private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + protected injector: Injector, + protected cd: ChangeDetectorRef, + protected componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -139,7 +139,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV return this.isEditValue; } - private init() { + protected init() { this.translations = this.entitiesTableConfig.entityTranslations; this.resources = this.entitiesTableConfig.entityResources; this.buildEntityComponent(); diff --git a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts index 8d868eff98..5da90b49ff 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts @@ -38,6 +38,8 @@ export abstract class EntityComponent, isEditValue: boolean; + isDetailsPage = false; + @Input() set entitiesTableConfig(entitiesTableConfig: C) { this.setEntitiesTableConfig(entitiesTableConfig); diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 73361eddce..0960d98620 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -147,6 +147,7 @@ import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; @NgModule({ declarations: @@ -155,6 +156,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa AddEntityDialogComponent, DetailsPanelComponent, EntityDetailsPanelComponent, + EntityDetailsPageComponent, AuditLogTableComponent, AuditLogDetailsDialogComponent, EventContentDialogComponent, @@ -282,6 +284,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa AddEntityDialogComponent, DetailsPanelComponent, EntityDetailsPanelComponent, + EntityDetailsPageComponent, AuditLogTableComponent, EventTableComponent, EdgeDownlinkTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index 20f271d660..2b73fc32c4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -38,6 +38,13 @@ (click)="editDeviceProfile($event)"> edit + DeviceProfileAutocompleteComponent), @@ -79,6 +80,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @Input() addNewProfile = true; + @Input() + showDetailsPageLink = false; + @Input() transportType: DeviceTransportType = null; @@ -110,6 +114,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, filteredDeviceProfiles: Observable>; searchText = ''; + deviceProfileURL: string; private dirty = false; @@ -240,6 +245,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new DeviceProfileId(profile.id.id); + this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); this.deviceProfileChanged.emit(profile); } @@ -278,6 +284,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null; if (!entityIdEquals(this.modelValue, idValue)) { this.modelValue = idValue; + this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.propagateChange(this.modelValue); this.deviceProfileChanged.emit(deviceProfile); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 28559dc4f7..3693c82f8f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -130,7 +130,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu setDisabledState(isDisabled: boolean){ this.disabled = isDisabled; if (this.disabled){ - this.provisionConfigurationFormGroup.disable(); + this.provisionConfigurationFormGroup.disable({emitEvent: false}); } else { if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) { this.provisionConfigurationFormGroup.enable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 917ad25dd2..df00bdf8fa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -16,6 +16,12 @@ -->
+ + TenantProfileAutocompleteComponent), @@ -66,6 +67,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, @Input() disabled: boolean; + @Input() + showDetailsPageLink = false; + @Output() tenantProfileUpdated = new EventEmitter(); @@ -74,6 +78,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, filteredTenantProfiles: Observable>; searchText = ''; + tenantProfileURL: string; private dirty = false; @@ -123,6 +128,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, (profile) => { if (profile) { this.modelValue = new TenantProfileId(profile.id.id); + this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); this.propagateChange(this.modelValue); } @@ -133,6 +139,11 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.selectTenantProfileFormGroup.disable(); + } else { + this.selectTenantProfileFormGroup.enable(); + } } writeValue(value: TenantProfileId | null): void { @@ -141,6 +152,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new TenantProfileId(profile.id.id); + this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); } ); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e5a3a54293..f98d0b6a80 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -16,6 +16,12 @@ -->
+ + diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss new file mode 100644 index 0000000000..38af76b016 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host{ + .mat-icon-button a { + border-bottom: none; + color: inherit; + } +} diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts index 6609d4ceb7..4d22d1f298 100644 --- a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts @@ -31,12 +31,13 @@ import { OtaPackageInfo, OtaUpdateTranslation, OtaUpdateType } from '@shared/mod import { OtaPackageService } from '@core/http/ota-package.service'; import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; -import { emptyPageData } from "@shared/models/page/page-data"; +import { emptyPageData } from '@shared/models/page/page-data'; +import { getEntityDetailsPageURL } from '@core/utils'; @Component({ selector: 'tb-ota-package-autocomplete', templateUrl: './ota-package-autocomplete.component.html', - styleUrls: [], + styleUrls: ['./ota-package-autocomplete.component.scss'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => OtaPackageAutocompleteComponent), @@ -64,6 +65,9 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On @Input() useFullEntityId = false; + @Input() + showDetailsPageLink = false; + private requiredValue: boolean; get required(): boolean { @@ -83,6 +87,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On filteredPackages: Observable>; searchText = ''; + packageURL: string; private dirty = false; @@ -166,6 +171,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On if (packageId !== '') { this.entityService.getEntity(EntityType.OTA_PACKAGE, packageId, {ignoreLoading: true, ignoreErrors: true}).subscribe( (entity) => { + this.packageURL = getEntityDetailsPageURL(entity.id.id, EntityType.OTA_PACKAGE); this.modelValue = this.useFullEntityId ? entity.id : entity.id.id; this.otaPackageFormGroup.get('packageId').patchValue(entity, {emitEvent: false}); }, diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9bf120f8b7..fa73e0d40e 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -16,6 +16,7 @@ import { InjectionToken } from '@angular/core'; import { IModulesMap } from '@modules/common/modules-map.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; export const Constants = { serverErrorCode: { @@ -137,6 +138,20 @@ export const HelpLinks = { } }; +export const baseDetailsPageByEntityType = new Map([ + [EntityType.TENANT, '/tenants'], + [EntityType.TENANT_PROFILE, '/tenantProfiles'], + [EntityType.CUSTOMER, '/customers'], + [EntityType.USER, '/users'], + [EntityType.ASSET, '/assets'], + [EntityType.DEVICE, '/devices'], + [EntityType.DEVICE_PROFILE, '/deviceProfiles'], + [EntityType.EDGE, '/edgeInstances'], + [EntityType.ENTITY_VIEW, '/entityViews'], + [EntityType.TB_RESOURCE, '/settings/resources-library'], + [EntityType.OTA_PACKAGE, '/otaUpdates'] +]); + export interface ValueTypeData { name: string; icon: string; diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index c9037a5dab..b1601753d8 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -89,4 +89,5 @@ export interface EntityRelationInfo extends EntityRelation { toEntityTypeName?: string; toName: string; fromEntityTypeName?: string; + entityURL?: string; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 086768c854..50975b9f11 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -455,6 +455,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyboardShortcutPipe, FileSizePipe, SelectableColumnsPipe, + RouterModule, TranslateModule, JsonObjectEditDialogComponent, HistorySelectorComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 807f3781a9..4d3a51d087 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -622,7 +622,8 @@ "enter-search": "Enter search", "created-time": "Created time", "loading": "Loading...", - "proceed": "Proceed" + "proceed": "Proceed", + "open-details-page": "Open details page" }, "content-type": { "json": "Json", From b58b3a303b4861f71635faa626eaa9d81021bec2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 28 Dec 2021 18:37:37 +0200 Subject: [PATCH 030/403] UI: Added support delete entity in details page --- .../entity/entity-details-page.component.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index 27ed899c79..c2878680e5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -35,6 +35,7 @@ import { TranslateService } from '@ngx-translate/core'; import { deepClone, mergeDeep } from '@core/utils'; import { BroadcastService } from '@core/services/broadcast.service'; import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; +import { DialogService } from '@core/services/dialog.service'; @Component({ selector: 'tb-entity-details-page', @@ -72,6 +73,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl protected componentFactoryResolver: ComponentFactoryResolver, private broadcast: BroadcastService, private translate: TranslateService, + private dialogService: DialogService, protected store: Store) { super(store, injector, cd, componentFactoryResolver); this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; @@ -85,6 +87,11 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); super.init(); this.entityComponent.isDetailsPage = true; + this.subscriptions.push(this.entityAction.subscribe((action) => { + if (action.action === 'delete') { + this.deleteEntity(action.event, action.entity); + } + })); } ngOnDestroy() { @@ -151,4 +158,25 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl confirmForm(): FormGroup { return this.detailsForm; } + + private deleteEntity($event: Event, entity: BaseData) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.entitiesTableConfig.deleteEntityTitle(entity), + this.entitiesTableConfig.deleteEntityContent(entity), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.entitiesTableConfig.deleteEntity(entity.id).subscribe( + () => { + this.router.navigate(['../'], {relativeTo: this.route}); + } + ); + } + }); + } } From d7f9b1d430fbb9009346da5458c522a862cd1b5e Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 29 Dec 2021 12:59:39 +0200 Subject: [PATCH 031/403] UI: Disable column sort if enable post processing func --- .../widget/lib/entities-table-widget.component.html | 2 +- .../components/widget/lib/entities-table-widget.component.ts | 4 ++++ .../home/components/widget/lib/table-widget.models.ts | 1 + .../widget/lib/timeseries-table-widget.component.html | 2 +- .../widget/lib/timeseries-table-widget.component.ts | 5 ++++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 74ccd53ec2..bfb775306b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -41,7 +41,7 @@
- {{ column.title }} + {{ column.title }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index bd52f2ab9b..a2e4f7b922 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -324,6 +324,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityName', def: 'entityName', title: entityNameColumnTitle, + sortable: true, entityKey: { key: 'name', type: EntityKeyType.ENTITY_FIELD @@ -347,6 +348,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityLabel', def: 'entityLabel', title: entityLabelColumnTitle, + sortable: true, entityKey: { key: 'label', type: EntityKeyType.ENTITY_FIELD @@ -370,6 +372,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityType', def: 'entityType', title: this.translate.instant('entity.entity-type'), + sortable: true, entityKey: { key: 'entityType', type: EntityKeyType.ENTITY_FIELD @@ -403,6 +406,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.title = dataKey.label; dataKey.def = 'def' + this.columns.length; + dataKey.sortable = !dataKey.usePostProcessing; const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.entityField && !isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index f90c287111..1d798a88ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -73,6 +73,7 @@ export interface EntityData { export interface EntityColumn extends DataKey { def: string; title: string; + sortable: boolean; entityKey?: EntityKey; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 8c3b9084b0..d209a649b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -52,7 +52,7 @@ - {{ h.dataKey.label }} + {{ h.dataKey.label }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index de8cec8dc7..198e36c919 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -88,6 +88,7 @@ interface TimeseriesRow { interface TimeseriesHeader { index: number; dataKey: DataKey; + sortable: boolean; } interface TimeseriesTableSource { @@ -308,10 +309,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI for (let a = 0; a < datasource.dataKeys.length; a++ ) { const dataKey = datasource.dataKeys[a]; const keySettings: TableWidgetDataKeySettings = dataKey.settings; + const sortable = !dataKey.usePostProcessing; const index = a + 1; source.header.push({ index, - dataKey + dataKey, + sortable }); source.displayedColumns.push(index + ''); source.rowDataTemplate[dataKey.label] = null; From b53746bda6bf246f5ad9c28f31b746de739f068a Mon Sep 17 00:00:00 2001 From: desoliture Date: Tue, 28 Dec 2021 16:38:05 +0200 Subject: [PATCH 032/403] MQTT Gateway API attributes request fix --- .../transport/mqtt/adaptors/JsonMqttAdaptor.java | 12 +++++++++--- .../mqtt/session/GatewayDeviceSessionCtx.java | 6 ++++++ .../mqtt/session/GatewaySessionHandler.java | 6 ++++++ .../common/transport/adaptor/JsonConverter.java | 15 +++++++++------ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index dc2b74a8ea..0548fa08fb 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.session.GatewayDeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; import java.nio.charset.Charset; @@ -123,7 +124,12 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { @Override public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { - return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); + JsonObject request = ((GatewayDeviceSessionCtx) ctx) + .getPendingAttributesRequests() + .getOrDefault(responseMsg.getRequestId(), new JsonObject()); + boolean multipleAttrKeysRequested = + request.has("keys") && request.get("keys").getAsJsonArray().size() > 1; + return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg, multipleAttrKeysRequested); } @Override @@ -232,11 +238,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - private Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + private Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg, boolean multipleAttrKeysRequested) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { - JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg); + JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg, multipleAttrKeysRequested); return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result)); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java index f41c5668cd..593205b745 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.mqtt.session; +import com.google.gson.JsonObject; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.mqtt.MqttMessage; import lombok.extern.slf4j.Slf4j; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; +import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentMap; @@ -136,6 +138,10 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple // This feature is not supported in the TB IoT Gateway yet. } + public Map getPendingAttributesRequests() { + return parent.getPendingAttributesRequests(); + } + private boolean isAckExpected(MqttMessage message) { return message.fixedHeader().qosLevel().value() > 0; } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 7b45b5f9d9..4b4e986d5f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -89,6 +89,7 @@ public class GatewaySessionHandler { private final ConcurrentMap mqttQoSMap; private final ChannelHandlerContext channel; private final DeviceSessionCtx deviceSessionCtx; + private final Map pendingAttributesRequests = new ConcurrentHashMap<>(); public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { this.context = deviceSessionCtx.getContext(); @@ -107,6 +108,10 @@ public class GatewaySessionHandler { return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); } + public Map getPendingAttributesRequests () { + return pendingAttributesRequests; + } + public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { if (isJsonPayloadType()) { onDeviceConnectJson(mqttMsg); @@ -558,6 +563,7 @@ public class GatewaySessionHandler { if (json.isJsonObject()) { JsonObject jsonObj = json.getAsJsonObject(); int requestId = jsonObj.get("id").getAsInt(); + pendingAttributesRequests.put(requestId, jsonObj); String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); boolean clientScope = jsonObj.get("client").getAsBoolean(); Set keys; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index be4143e388..86f90d786f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -313,16 +313,19 @@ public class JsonConverter { return result; } - public static JsonObject getJsonObjectForGateway(String deviceName, TransportProtos.GetAttributeResponseMsg - responseMsg) { + public static JsonObject getJsonObjectForGateway( + String deviceName, + TransportProtos.GetAttributeResponseMsg responseMsg, + boolean multipleAttrKeysRequested + ) { JsonObject result = new JsonObject(); result.addProperty("id", responseMsg.getRequestId()); result.addProperty(DEVICE_PROPERTY, deviceName); if (responseMsg.getClientAttributeListCount() > 0) { - addValues(result, responseMsg.getClientAttributeListList()); + addValues(result, responseMsg.getClientAttributeListList(), multipleAttrKeysRequested); } if (responseMsg.getSharedAttributeListCount() > 0) { - addValues(result, responseMsg.getSharedAttributeListList()); + addValues(result, responseMsg.getSharedAttributeListList(), multipleAttrKeysRequested); } return result; } @@ -335,8 +338,8 @@ public class JsonConverter { return result; } - private static void addValues(JsonObject result, List kvList) { - if (kvList.size() == 1) { + private static void addValues(JsonObject result, List kvList, boolean multipleAttrKeysRequested) { + if (kvList.size() == 1 && !multipleAttrKeysRequested) { addValueToJson(result, "value", kvList.get(0).getKv()); } else { JsonObject values; From 2f5648c400122b02838ea11d650dc55056dc860c Mon Sep 17 00:00:00 2001 From: desoliture Date: Wed, 29 Dec 2021 17:22:32 +0200 Subject: [PATCH 033/403] refactoring --- .../server/transport/mqtt/adaptors/JsonMqttAdaptor.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 0548fa08fb..7aea8f3f91 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -43,6 +43,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.Map; import java.util.UUID; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; @@ -124,11 +125,12 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { @Override public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { - JsonObject request = ((GatewayDeviceSessionCtx) ctx) - .getPendingAttributesRequests() - .getOrDefault(responseMsg.getRequestId(), new JsonObject()); + Map pendingAttributesRequests = ((GatewayDeviceSessionCtx) ctx).getPendingAttributesRequests(); + int requestId = responseMsg.getRequestId(); + JsonObject request = pendingAttributesRequests.getOrDefault(requestId, new JsonObject()); boolean multipleAttrKeysRequested = request.has("keys") && request.get("keys").getAsJsonArray().size() > 1; + pendingAttributesRequests.remove(requestId); return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg, multipleAttrKeysRequested); } From b82c27f4f725d29e27a9bbc24d0d5f6b2d783c7b Mon Sep 17 00:00:00 2001 From: zbeacon Date: Wed, 29 Dec 2021 17:39:11 +0200 Subject: [PATCH 034/403] Refactored to use persistent RPC --- .../DefaultGatewayDeviceStateService.java | 200 +++++------------- .../GatewayDeviceStateService.java | 2 - .../queue/DefaultTbClusterService.java | 11 +- .../rpc/DefaultTbCoreDeviceRpcService.java | 7 +- .../transport/DefaultTransportApiService.java | 6 - .../device-wizard-dialog.component.html | 6 - .../wizard/device-wizard-dialog.component.ts | 2 - .../home/pages/device/device.component.html | 6 - .../home/pages/device/device.component.ts | 2 - .../assets/locale/locale.constant-en_US.json | 1 - 10 files changed, 61 insertions(+), 182 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java index 57a0b36508..84b9f977ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java @@ -15,200 +15,98 @@ */ package org.thingsboard.server.service.gateway_device; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; +import com.fasterxml.jackson.databind.node.TextNode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; +import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.UUID; @Slf4j @Service @RequiredArgsConstructor public class DefaultGatewayDeviceStateService implements GatewayDeviceStateService { - private static final String RENAMED_GATEWAY_DEVICES = "renamedGatewayDevices"; - private static final String DELETED_GATEWAY_DEVICES = "deletedGatewayDevices"; - private static final String HANDLE_DEVICE_RENAMING_PARAMETER = "handleDeviceRenaming"; + private final static String DEVICE_RENAMED_METHOD_NAME = "gateway_device_renamed"; + private final static String DEVICE_DELETED_METHOD_NAME = "gateway_device_deleted"; - private final AttributesService attributesService; - private final RelationService relationService; - private final DeviceService deviceService; - @Lazy - @Autowired - private TelemetrySubscriptionService tsSubService; - @Override - public void update(Device device, Device oldDevice) { - List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); - if (!relationToGatewayList.isEmpty()) { - if (oldDevice != null) { - EntityRelation relationToGateway = relationToGatewayList.get(0); - - Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); - if (isHandleDeviceRenamingEnabled(gatewayDevice.getAdditionalInfo())) { - ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); - DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { - ObjectNode renamedGatewayDevicesNode; - KvEntry renamedGatewayDevicesKvEntry; - String newDeviceName = device.getName(); - String oldDeviceName = oldDevice.getName(); + @Value("${server.rest.server_side_rpc.min_timeout:5000}") + protected long minTimeout; - if (renamedGatewayDevicesList.isEmpty()) { - renamedGatewayDevicesNode = JacksonUtil.newObjectNode(); - renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); - } else { - AttributeKvEntry receivedRenamedGatewayDevicesAttribute = renamedGatewayDevicesList.get(0); - renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(receivedRenamedGatewayDevicesAttribute.getValueAsString()); - if (renamedGatewayDevicesNode.findValue(newDeviceName) != null && oldDeviceName.equals(renamedGatewayDevicesNode.get(newDeviceName).asText())) { - // If a new device name is the same like the first name or another device was renamed like some existing device - renamedGatewayDevicesNode.remove(newDeviceName); - } else { + @Value("${server.rest.server_side_rpc.default_timeout:10000}") + protected long defaultTimeout; - AtomicBoolean renamedFirstTime = new AtomicBoolean(true); + private final RelationService relationService; + private final DeviceService deviceService; - renamedGatewayDevicesNode.fields().forEachRemaining(entry -> { - // If device was renamed earlier - if (oldDeviceName.equals(entry.getValue().asText())) { - renamedGatewayDevicesNode.put(entry.getKey(), newDeviceName); - renamedFirstTime.set(false); - } - }); - if (renamedFirstTime.get()) { - renamedGatewayDevicesNode.put(oldDeviceName, newDeviceName); - } - } - } + @Lazy + @Autowired + private TbCoreDeviceRpcService deviceRpcService; - renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); - }, - e -> log.error("Cannot get gateway renamed devices attribute", e)); - } - } - } - } @Override - public void delete(Device device) { + public void update(Device device, Device oldDevice) { List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); if (!relationToGatewayList.isEmpty()) { EntityRelation relationToGateway = relationToGatewayList.get(0); - final String[] deletedDeviceName = {device.getName()}; - ListenableFuture> renamedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("renamedGatewayDevices")); - DonAsynchron.withCallback(renamedGatewayDevicesFuture, renamedGatewayDevicesList -> { - if (!renamedGatewayDevicesList.isEmpty()) { - ObjectNode renamedGatewayDevicesNode = (ObjectNode) JacksonUtil.toJsonNode(renamedGatewayDevicesList.get(0).getValueAsString()); - if (renamedGatewayDevicesNode != null) { - AtomicBoolean renamedListChanged = new AtomicBoolean(false); - if (renamedGatewayDevicesNode.findValue(deletedDeviceName[0]) != null) { - renamedGatewayDevicesNode.remove(deletedDeviceName[0]); - renamedListChanged.set(true); - } - Map renamedGatewayDevicesMap = JacksonUtil.OBJECT_MAPPER.convertValue(renamedGatewayDevicesNode, new TypeReference<>() { - }); - renamedGatewayDevicesMap.forEach((key, value) -> { - // If device was renamed earlier - if (deletedDeviceName[0].equals(value)) { - renamedGatewayDevicesNode.remove(key); - deletedDeviceName[0] = key; - renamedListChanged.set(true); - } - }); - if (renamedListChanged.get()) { - KvEntry renamedGatewayDevicesKvEntry = new JsonDataEntry(RENAMED_GATEWAY_DEVICES, JacksonUtil.toString(renamedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, renamedGatewayDevicesKvEntry); - } - } - } - }, e -> log.error("Cannot get gateway renamed devices attribute", e)); - ListenableFuture> deletedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("deletedGatewayDevices")); - DonAsynchron.withCallback(deletedGatewayDevicesFuture, deletedGatewayDevicesList -> { - ArrayNode deletedGatewayDevicesNode; - if (!deletedGatewayDevicesList.isEmpty()) { - deletedGatewayDevicesNode = (ArrayNode) JacksonUtil.toJsonNode(deletedGatewayDevicesList.get(0).getValueAsString()); - } else { - deletedGatewayDevicesNode = JacksonUtil.OBJECT_MAPPER.createArrayNode(); - } - deletedGatewayDevicesNode.add(deletedDeviceName[0]); - KvEntry deletedGatewayDevicesKvEntry = new JsonDataEntry(DELETED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, deletedGatewayDevicesKvEntry); - }, e -> log.error("Cannot get gateway deleted devices attribute", e)); + Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + ObjectNode renamedDeviceNode = JacksonUtil.newObjectNode(); + renamedDeviceNode.put(device.getName(), oldDevice.getName()); + ToDeviceRpcRequest rpcRequest = formDeviceToGatewayRPCRequest(gatewayDevice, renamedDeviceNode, DEVICE_RENAMED_METHOD_NAME); + deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> { + log.trace("Device renamed RPC with id: [{}] processed to gateway device with id: [{}], old device name: [{}], new device name: [{}]", + rpcRequest.getId(), gatewayDevice.getId(), oldDevice.getName(), device.getName()); + }, null); } } @Override - public void checkAndUpdateDeletedGatewayDevicesAttribute(Device device) { + public void delete(Device device) { List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); if (!relationToGatewayList.isEmpty()) { EntityRelation relationToGateway = relationToGatewayList.get(0); - ListenableFuture> deletedGatewayDevicesFuture = attributesService.find(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, Collections.singletonList("deletedGatewayDevices")); - DonAsynchron.withCallback(deletedGatewayDevicesFuture, deletedGatewayDevicesList -> { - ArrayNode deletedGatewayDevicesNode; - if (!deletedGatewayDevicesList.isEmpty()) { - int deletedDeviceIndex = -1; - deletedGatewayDevicesNode = (ArrayNode) JacksonUtil.toJsonNode(deletedGatewayDevicesList.get(0).getValueAsString()); - for (int i = 0; i < deletedGatewayDevicesNode.size(); i++) { - if (deletedGatewayDevicesNode.get(i).asText().equals(device.getName())) { - deletedDeviceIndex = i; - } - } - if (deletedDeviceIndex != -1) { - deletedGatewayDevicesNode.remove(deletedDeviceIndex); - KvEntry deletedGatewayDevicesKvEntry = new JsonDataEntry(DELETED_GATEWAY_DEVICES, JacksonUtil.toString(deletedGatewayDevicesNode)); - saveGatewayDevicesAttribute(device, relationToGateway, deletedGatewayDevicesKvEntry); - } - } - }, e -> log.error("Cannot get gateway deleted devices attribute", e)); + Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + TextNode deletedDeviceNode = new TextNode(device.getName()); + ToDeviceRpcRequest rpcRequest = formDeviceToGatewayRPCRequest(gatewayDevice, deletedDeviceNode, DEVICE_DELETED_METHOD_NAME); + deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> { + log.trace("Device deleted RPC with id: [{}] processed to gateway device with id: [{}], deleted device name: [{}]", + rpcRequest.getId(), gatewayDevice.getId(), device.getName()); + }, null); } } - private void saveGatewayDevicesAttribute(Device device, EntityRelation relationToGateway, KvEntry gatewayDevicesKvEntry) { - AttributeKvEntry attrKvEntry = new BaseAttributeKvEntry(System.currentTimeMillis(), gatewayDevicesKvEntry); - tsSubService.saveAndNotify(device.getTenantId(), relationToGateway.getTo(), DataConstants.SHARED_SCOPE, List.of(attrKvEntry), true, new FutureCallback() { - @Override - public void onSuccess(@Nullable Void unused) { - log.trace("Attribute saved for gateway with ID [{}] and data [{}]", relationToGateway.getTo(), gatewayDevicesKvEntry.getJsonValue()); - } - - @Override - public void onFailure(Throwable throwable) { - log.error("Cannot save gateway device attribute", throwable); - } - }); - } - - private boolean isHandleDeviceRenamingEnabled(JsonNode additionalInfo) { - if (additionalInfo.get(HANDLE_DEVICE_RENAMING_PARAMETER) != null) { - return additionalInfo.get(HANDLE_DEVICE_RENAMING_PARAMETER).asBoolean(); - } - return false; + private ToDeviceRpcRequest formDeviceToGatewayRPCRequest(Device gatewayDevice, JsonNode deviceDataNode, String method) { + ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(method, JacksonUtil.toString(deviceDataNode)); + long expTime = System.currentTimeMillis() + Math.max(minTimeout, defaultTimeout); + UUID rpcRequestUUID = UUID.randomUUID(); + return new ToDeviceRpcRequest(rpcRequestUUID, + gatewayDevice.getTenantId(), + gatewayDevice.getId(), + true, + expTime, + body, + true, + null, + null + ); } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java index d4079487a5..1c06967cff 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/GatewayDeviceStateService.java @@ -22,6 +22,4 @@ public interface GatewayDeviceStateService { void update(Device device, Device oldDevice); void delete(Device device); - - void checkAndUpdateDeletedGatewayDevicesAttribute(Device device); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index be44c9ee40..4ffc782803 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -401,13 +401,18 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceUpdated(Device device, Device old, boolean notifyEdge) { var created = old == null; broadcastEntityChangeToTransport(device.getTenantId(), device.getId(), device, null); - if (old != null && (!device.getName().equals(old.getName()) || !device.getType().equals(old.getType()))) { - pushMsgToCore(new DeviceNameOrTypeUpdateMsg(device.getTenantId(), device.getId(), device.getName(), device.getType()), null); + if (old != null) { + boolean deviceNameChanged = !device.getName().equals(old.getName()); + if (deviceNameChanged) { + gatewayDeviceStateService.update(device, old); + } + if (deviceNameChanged || !device.getType().equals(old.getType())) { + pushMsgToCore(new DeviceNameOrTypeUpdateMsg(device.getTenantId(), device.getId(), device.getName(), device.getType()), null); + } } broadcastEntityStateChangeEvent(device.getTenantId(), device.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); sendDeviceStateServiceEvent(device.getTenantId(), device.getId(), created, !created, false); otaPackageStateService.update(device, old); - gatewayDeviceStateService.update(device, old); if (!created && notifyEdge) { sendNotificationMsgToEdgeService(device.getTenantId(), null, device.getId(), null, null, EdgeEventActionType.UPDATED); } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java index 2b2ade02ee..99e013d6d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java @@ -22,10 +22,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; -import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -34,7 +36,6 @@ import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.service.security.model.SecurityUser; import javax.annotation.PostConstruct; @@ -183,7 +184,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { entityNode.put(DataConstants.ADDITIONAL_INFO, msg.getAdditionalInfo()); try { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), currentUser.getCustomerId(), metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode)); clusterService.pushMsgToRuleEngine(msg.getTenantId(), msg.getDeviceId(), tbMsg, null); } catch (JsonProcessingException e) { throw new RuntimeException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 929d87e5f4..04e581ba6b 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -95,11 +95,9 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; -import org.thingsboard.server.service.gateway_device.GatewayDeviceStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.service.resource.TbResourceService; -import org.thingsboard.server.service.state.DeviceStateService; import java.util.List; import java.util.Optional; @@ -130,7 +128,6 @@ public class DefaultTransportApiService implements TransportApiService { private final DeviceService deviceService; private final RelationService relationService; private final DeviceCredentialsService deviceCredentialsService; - private final DeviceStateService deviceStateService; private final DbCallbackExecutorService dbCallbackExecutorService; private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; @@ -138,7 +135,6 @@ public class DefaultTransportApiService implements TransportApiService { private final TbResourceService resourceService; private final OtaPackageService otaPackageService; private final OtaPackageDataCache otaPackageDataCache; - private final GatewayDeviceStateService gatewayDeviceStateService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -319,8 +315,6 @@ public class DefaultTransportApiService implements TransportApiService { } relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, lastConnectedGatewayRelation); - gatewayDeviceStateService.checkAndUpdateDeletedGatewayDevicesAttribute(device); - GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() .setDeviceInfo(getDeviceInfoProto(device)); DeviceProfile deviceProfile = deviceProfileCache.get(device.getTenantId(), device.getDeviceProfileId()); diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index cac8ae4c1e..ae0be158d5 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -103,16 +103,10 @@ {{ 'device.is-gateway' | translate }} - -
{{ 'device.overwrite-activity-time' | translate }} - - {{ 'device.handle-device-renaming' | translate }} -
device.description diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 552e1d5eb8..fdbc25107f 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -109,7 +109,6 @@ export class DeviceWizardDialogComponent extends label: ['', Validators.maxLength(255)], gateway: [false], overwriteActivityTime: [false], - handleDeviceRenaming: [false], addProfileType: [0], deviceProfileId: [null, Validators.required], newDeviceProfileTitle: [{value: null, disabled: true}], @@ -327,7 +326,6 @@ export class DeviceWizardDialogComponent extends additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value, - handleDeviceRenaming: this.deviceWizardFormGroup.get('handleDeviceRenaming').value, description: this.deviceWizardFormGroup.get('description').value }, customerId: null diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 1f89481fd0..1187a34f5d 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -128,16 +128,10 @@ {{ 'device.is-gateway' | translate }} - -
{{ 'device.overwrite-activity-time' | translate }} - - {{ 'device.handle-device-renaming' | translate }} -
device.description diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index cd1c78b3dd..7cd60a89f4 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -92,7 +92,6 @@ export class DeviceComponent extends EntityComponent { { gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], overwriteActivityTime: [entity && entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false], - handleDeviceRenaming: [entity && entity.additionalInfo ? entity.additionalInfo.handleDeviceRenaming : false], description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], } ) @@ -122,7 +121,6 @@ export class DeviceComponent extends EntityComponent { additionalInfo: { gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false, overwriteActivityTime: entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false, - handleDeviceRenaming: entity.additionalInfo ? entity.additionalInfo.handleDeviceRenaming : false, description: entity.additionalInfo ? entity.additionalInfo.description : '' } }); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 092f944ece..807f3781a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1045,7 +1045,6 @@ "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", "is-gateway": "Is gateway", "overwrite-activity-time": "Overwrite activity time for connected device", - "handle-device-renaming": "Handle device renaming", "public": "Public", "device-public": "Device is public", "select-device": "Select device", From e69871d8869af3513a746119d14e884939d57bb5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 29 Dec 2021 17:50:56 +0200 Subject: [PATCH 035/403] UI: Added constants in baseDetailsPageByEntityType --- ui-ngx/src/app/shared/models/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index fa73e0d40e..251f310f9b 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -143,9 +143,11 @@ export const baseDetailsPageByEntityType = new Map([ [EntityType.TENANT_PROFILE, '/tenantProfiles'], [EntityType.CUSTOMER, '/customers'], [EntityType.USER, '/users'], + [EntityType.DASHBOARD, '/dashboards'], [EntityType.ASSET, '/assets'], [EntityType.DEVICE, '/devices'], [EntityType.DEVICE_PROFILE, '/deviceProfiles'], + [EntityType.RULE_CHAIN, '/ruleChains'], [EntityType.EDGE, '/edgeInstances'], [EntityType.ENTITY_VIEW, '/entityViews'], [EntityType.TB_RESOURCE, '/settings/resources-library'], From d255c8d16c5b534c77a24905126dead39f4abe48 Mon Sep 17 00:00:00 2001 From: zbeacon Date: Thu, 30 Dec 2021 12:27:45 +0200 Subject: [PATCH 036/403] Refactored --- .../DefaultGatewayDeviceStateService.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java index 84b9f977ba..4fa6be5654 100644 --- a/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/gateway_device/DefaultGatewayDeviceStateService.java @@ -48,7 +48,6 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi private final static String DEVICE_RENAMED_METHOD_NAME = "gateway_device_renamed"; private final static String DEVICE_DELETED_METHOD_NAME = "gateway_device_deleted"; - @Value("${server.rest.server_side_rpc.min_timeout:5000}") protected long minTimeout; @@ -62,13 +61,10 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi @Autowired private TbCoreDeviceRpcService deviceRpcService; - @Override public void update(Device device, Device oldDevice) { - List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); - if (!relationToGatewayList.isEmpty()) { - EntityRelation relationToGateway = relationToGatewayList.get(0); - Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + Device gatewayDevice = findGatewayDeviceByRelationFromDevice(device); + if (gatewayDevice != null) { ObjectNode renamedDeviceNode = JacksonUtil.newObjectNode(); renamedDeviceNode.put(device.getName(), oldDevice.getName()); ToDeviceRpcRequest rpcRequest = formDeviceToGatewayRPCRequest(gatewayDevice, renamedDeviceNode, DEVICE_RENAMED_METHOD_NAME); @@ -81,10 +77,8 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi @Override public void delete(Device device) { - List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); - if (!relationToGatewayList.isEmpty()) { - EntityRelation relationToGateway = relationToGatewayList.get(0); - Device gatewayDevice = deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + Device gatewayDevice = findGatewayDeviceByRelationFromDevice(device); + if (gatewayDevice != null) { TextNode deletedDeviceNode = new TextNode(device.getName()); ToDeviceRpcRequest rpcRequest = formDeviceToGatewayRPCRequest(gatewayDevice, deletedDeviceNode, DEVICE_DELETED_METHOD_NAME); deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> { @@ -109,4 +103,13 @@ public class DefaultGatewayDeviceStateService implements GatewayDeviceStateServi null ); } + + private Device findGatewayDeviceByRelationFromDevice(Device device) { + List relationToGatewayList = relationService.findByFromAndType(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.LAST_CONNECTED_GATEWAY, RelationTypeGroup.COMMON); + if (!relationToGatewayList.isEmpty()) { + EntityRelation relationToGateway = relationToGatewayList.get(0); + return deviceService.findDeviceById(device.getTenantId(), (DeviceId) relationToGateway.getTo()); + } + return null; + } } From c655b58977990fc57bfe570509c1a7faf2e88e8d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 30 Dec 2021 12:31:21 +0200 Subject: [PATCH 037/403] lwm2m delete security files jks and in yml security enable = false --- .../src/main/resources/thingsboard.yml | 10 +- .../credentials/shell/lwM2M_credentials.sh | 359 ----------------- .../credentials/shell/lwM2M_keygen.properties | 57 --- .../lwm2m/src/main/resources/lwm2mserver.jks | Bin 3849 -> 0 bytes pom.xml | 34 -- .../credentials/shell/lwM2M_credentials.sh | 360 ------------------ .../credentials/shell/lwM2M_keygen.properties | 57 --- transport/lwm2m/src/main/data/lwm2mserver.jks | Bin 4017 -> 0 bytes .../src/main/resources/tb-lwm2m-transport.yml | 10 +- 9 files changed, 10 insertions(+), 877 deletions(-) delete mode 100644 common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh delete mode 100644 common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties delete mode 100644 common/transport/lwm2m/src/main/resources/lwm2mserver.jks delete mode 100755 transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh delete mode 100644 transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties delete mode 100644 transport/lwm2m/src/main/data/lwm2mserver.jks diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a502afb297..6865aebf9a 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -733,7 +733,7 @@ transport: # Server X509 Certificates support credentials: # Whether to enable LWM2M server X509 Certificate/RPK support - enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -769,7 +769,7 @@ transport: # Bootstrap server X509 Certificates support credentials: # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support - enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -796,19 +796,19 @@ transport: # X509 trust certificates trust-credentials: # Whether to load X509 trust certificates - enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}" # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" # PEM certificates pem: # Path to the certificates file (holds trust certificates) - cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: # Type of the key store type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates - store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" # Password used to access the key store store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh deleted file mode 100644 index f68ca30005..0000000000 --- a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh +++ /dev/null @@ -1,359 +0,0 @@ -#!/bin/sh -# -# Copyright © 2016-2021 The Thingsboard Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password - -#p) CLIENT_CN=$CLIENT_PREFIX00000000 -#s) client_start=0 -#f) client_finish=1 -#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 -#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 -#b) BOOTSTRAP_ALIAS=bootstrap -#d) SERVER_ALIAS=server -#j) SERVER_STORE=serverKeyStore.jks -#k) CLIENT_STORE=clientKeyStore.jks -#c) CLIENT_STORE_PWD=client_ks_password -#w) SERVER_STORE_PWD=server_ks_password -#l) ROOT_KEY_ALIAS=root_key_alias - -while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do - case "${flag}" in - p) client_pref=${OPTARG} ;; - s) client_start=${OPTARG} ;; - f) client_finish=${OPTARG} ;; - a) client_alias_pref=${OPTARG} ;; - e) client_self_alias_pref=${OPTARG} ;; - b) bootstrap_alias=${OPTARG} ;; - d) server_alias=${OPTARG} ;; - j) key_store_server_file=${OPTARG} ;; - k) key_store_client_file=${OPTARG} ;; - c) client_key_store_pwd=${OPTARG} ;; - w) server_key_store_pwd=${OPTARG} ;; - w) root_key_alias=${OPTARG} ;; - esac -done - -# cd to dir of script -script_dir=$(dirname $0) -echo "script_dir: $script_dir" -cd $script_dir -# source the properties: -. ./lwM2M_keygen.properties - -if [ -n "$client_pref" ]; then - CLIENT_PREFIX=$client_pref -fi - -if [ -z "$client_start" ]; then - client_start=0 -fi - -if [ -z "$client_finish" ]; then - client_finish=1 -fi - -if [ -n "$client_alias_pref" ]; then - CLIENT_ALIAS_PREFIX=$client_alias_pref -fi - -if [ -n "$client_self_alias_pref" ]; then - CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref -fi - -if [ -n "$bootstrap_alias" ]; then - BOOTSTRAP_ALIAS=$bootstrap_alias -fi - -if [ -n "$server_alias" ]; then - SERVER_ALIAS=$server_alias -fi - -if [ -n "$key_store_server_file" ]; then - SERVER_STORE=$key_store_server_file -fi - -if [ -n "$key_store_client_file" ]; then - CLIENT_STORE=$key_store_client_file -fi - -if [ -n "$client_key_store_pwd" ]; then - CLIENT_STORE_PWD=$client_key_store_pwd -fi - -if [ -n "$server_key_store_pwd" ]; then - SERVER_STORE_PWD=$server_key_store_pwd -fi - -if [ -n "$root_key_alias" ]; then - ROOT_KEY_ALIAS=$root_key_alias -fi - -CLIENT_NUMBER=$client_start - -echo "==Start==" -echo "CLIENT_PREFIX: $CLIENT_PREFIX" -echo "client_start: $client_start" -echo "client_finish: $client_finish" -echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" -echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" -echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" -echo "SERVER_ALIAS: $SERVER_ALIAS" -echo "SERVER_STORE: $SERVER_STORE" -echo "CLIENT_STORE: $CLIENT_STORE" -echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" -echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" -echo "CLIENT_NUMBER: $CLIENT_NUMBER" -echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" - -end_point() { - echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_alias_point() { - echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_self_alias_point() { - echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -# Generation of the keystore. -echo "${H0}====START========${RESET}" -echo "${H1}Server Keystore : ${RESET}" -echo "${H1}==================${RESET}" -echo "${H2}Creating the trusted root CA key and certificate...${RESET}" -# -keysize -# 1024 (when using -genkeypair) -keytool \ - -genkeypair \ - -alias $ROOT_KEY_ALIAS \ - -keyalg EC \ - -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating server key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $SERVER_ALIAS \ - -keyalg EC \ - -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $SERVER_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating server certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $SERVER_ALIAS \ - -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $BOOTSTRAP_ALIAS \ - -keyalg EC \ - -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $BOOTSTRAP_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $BOOTSTRAP_ALIAS \ - -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" - keytool \ - -exportcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -fi - -cert_end_point() { - echo - echo "${H1}Client Keystore : ${RESET}" - echo "${H1}==================${RESET}" - echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" - keytool \ - -genkeypair \ - -alias $CLIENT_ALIAS \ - -keyalg EC \ - -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $CLIENT_STORE_PWD \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD - keytool \ - -exportcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -importcert \ - -alias $CLIENT_SELF_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -# -# echo -# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" -# keytool \ -# -exportcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $SERVER_STORE \ -# -storepass $SERVER_STORE_PWD | -# keytool \ -# -importcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $CLIENT_STORE \ -# -storepass $CLIENT_STORE_PWD \ -# -noprompt -# - - echo - echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" - keytool \ - -certreq \ - -alias $CLIENT_ALIAS \ - -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -} - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "==Start Client==" - while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do - echo "number $CLIENT_NUMBER" - echo "finish $client_finish" - CLIENT_CN=$(end_point) - CLIENT_ALIAS=$(client_alias_point) - CLIENT_SELF_ALIAS=$(client_self_alias_point) - echo "CLIENT_CN $CLIENT_CN" - echo "CLIENT_ALIAS $CLIENT_ALIAS" - echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" - cert_end_point - CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) - echo - done -fi - -echo -echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" -keytool \ - -importkeystore \ - -srckeystore $SERVER_STORE \ - -destkeystore $SERVER_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" - keytool \ - -importkeystore \ - -srckeystore $CLIENT_STORE \ - -destkeystore $CLIENT_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $CLIENT_STORE_PWD -fi diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties deleted file mode 100644 index 7b3cd9c09a..0000000000 --- a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright © 2016-2017 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. -# - -# Keystore common parameters -ROOT_KEY_ALIAS=rootCA -DOMAIN_SUFFIX="$(hostname)" -ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" -ORGANIZATIONAL_UNIT=Thingsboard -ORGANIZATION=Thingsboard -CITY=SF -STATE_OR_PROVINCE=CA -TWO_LETTER_COUNTRY_CODE=US -VALIDITY=36500 #days -STORETYPE="JKS" - -#Server -SERVER_STORE=serverKeyStore1.jks -SERVER_STORE_PWD=server_ks_password1 -SERVER_ALIAS=server1 -SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" -SERVER_SELF_ALIAS=server_self_signed -SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" -BOOTSTRAP_ALIAS=bootstrap1 -BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" -BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed -BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" - -# Client -CLIENT_STORE=clientKeyStore1.jks -CLIENT_STORE_PWD=client_ks_password1 -CLIENT_ALIAS_PREFIX=client_alias_1 -CLIENT_PREFIX=LwX509___ -CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 -CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" - -# Color output stuff -red=`tput setaf 1` -green=`tput setaf 2` -blue=`tput setaf 4` -bold=`tput bold` -H0=${red}${bold} -H1=${green}${bold} -H2=${blue} -RESET=`tput sgr0` diff --git a/common/transport/lwm2m/src/main/resources/lwm2mserver.jks b/common/transport/lwm2m/src/main/resources/lwm2mserver.jks deleted file mode 100644 index 5fab824aa1b19928fe711701cd27857472a9e739..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3849 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 b(C|b)G3l|ClS0(tPLrtJO%hUKOzz(RVYoVD diff --git a/pom.xml b/pom.xml index c06960c6ca..9dd8c043b2 100755 --- a/pom.xml +++ b/pom.xml @@ -403,39 +403,9 @@ false - - ../common/transport/lwm2m/src/main/resources - - **/*.xml - **/*.jks - - false - - - - - - copy-lwm2m-resources - ${pkg.process-resources.phase} - - copy-resources - - - ../transport/lwm2m/src/main/data - - - ../common/transport/lwm2m/src/main/resources - - **/*.xml - **/*.jks - - false - - - copy-docker-config ${pkg.process-resources.phase} @@ -829,10 +799,6 @@ **/*.proto.js docker/haproxy/** docker/tb-node/** - src/main/resources/models/*.xml - src/main/resources/credentials/*.jks - src/main/resources/credentials/shell/*.jks - src/main/resources/credentials/shell/*.jks.old ui/** src/.browserslistrc **/yarn.lock diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh deleted file mode 100755 index d623bfad42..0000000000 --- a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/bin/sh -# -# Copyright © 2016-2021 The Thingsboard Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password - -#p) CLIENT_CN=$CLIENT_PREFIX00000000 -#s) client_start=0 -#f) client_finish=1 -#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 -#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 -#b) BOOTSTRAP_ALIAS=bootstrap -#d) SERVER_ALIAS=server -#j) SERVER_STORE=serverKeyStore.jks -#k) CLIENT_STORE=clientKeyStore.jks -#c) CLIENT_STORE_PWD=client_ks_password -#w) SERVER_STORE_PWD=server_ks_password -#l) ROOT_KEY_ALIAS=root_key_alias - -while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do - case "${flag}" in - p) client_pref=${OPTARG} ;; - s) client_start=${OPTARG} ;; - f) client_finish=${OPTARG} ;; - a) client_alias_pref=${OPTARG} ;; - e) client_self_alias_pref=${OPTARG} ;; - b) bootstrap_alias=${OPTARG} ;; - d) server_alias=${OPTARG} ;; - j) key_store_server_file=${OPTARG} ;; - k) key_store_client_file=${OPTARG} ;; - c) client_key_store_pwd=${OPTARG} ;; - w) server_key_store_pwd=${OPTARG} ;; - w) root_key_alias=${OPTARG} ;; - esac -done - -# cd to dir of script -script_dir=$(dirname $0) -echo "script_dir: $script_dir" -cd $script_dir -# source the properties: -. ./lwM2M_keygen.properties - -if [ -n "$client_pref" ]; then - CLIENT_PREFIX=$client_pref -fi - -if [ -z "$client_start" ]; then - client_start=0 -fi - -if [ -z "$client_finish" ]; then - client_finish=1 -fi - -if [ -n "$client_alias_pref" ]; then - CLIENT_ALIAS_PREFIX=$client_alias_pref -fi - -if [ -n "$client_self_alias_pref" ]; then - CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref -fi - -if [ -n "$bootstrap_alias" ]; then - BOOTSTRAP_ALIAS=$bootstrap_alias -fi - -if [ -n "$server_alias" ]; then - SERVER_ALIAS=$server_alias -fi - -if [ -n "$key_store_server_file" ]; then - SERVER_STORE=$key_store_server_file -fi - -if [ -n "$key_store_client_file" ]; then - CLIENT_STORE=$key_store_client_file -fi - -if [ -n "$client_key_store_pwd" ]; then - CLIENT_STORE_PWD=$client_key_store_pwd -fi - -if [ -n "$server_key_store_pwd" ]; then - SERVER_STORE_PWD=$server_key_store_pwd -fi - -if [ -n "$root_key_alias" ]; then - ROOT_KEY_ALIAS=$root_key_alias -fi - -CLIENT_NUMBER=$client_start - -echo "==Start==" -echo "CLIENT_PREFIX: $CLIENT_PREFIX" -echo "client_start: $client_start" -echo "client_finish: $client_finish" -echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" -echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" -echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" -echo "SERVER_ALIAS: $SERVER_ALIAS" -echo "SERVER_STORE: $SERVER_STORE" -echo "CLIENT_STORE: $CLIENT_STORE" -echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" -echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" -echo "CLIENT_NUMBER: $CLIENT_NUMBER" -echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" - -end_point() { - echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_alias_point() { - echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_self_alias_point() { - echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -# Generation of the keystore. -echo "${H0}====START========${RESET}" -echo "${H1}Server Keystore : ${RESET}" -echo "${H1}==================${RESET}" -echo "${H2}Creating the trusted root CA key and certificate...${RESET}" -# -keysize -# 1024 (when using -genkeypair) -keytool \ - -genkeypair \ - -alias $ROOT_KEY_ALIAS \ - -keyalg EC \ - -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating server key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $SERVER_ALIAS \ - -keyalg EC \ - -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $SERVER_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating server certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $SERVER_ALIAS \ - -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $BOOTSTRAP_ALIAS \ - -keyalg EC \ - -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $BOOTSTRAP_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $BOOTSTRAP_ALIAS \ - -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" - keytool \ - -exportcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -fi - -cert_end_point() { - echo - echo "${H1}Client Keystore : ${RESET}" - echo "${H1}==================${RESET}" - echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" - keytool \ - -genkeypair \ - -alias $CLIENT_ALIAS \ - -keyalg EC \ - -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $CLIENT_STORE_PWD \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD - keytool \ - -exportcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -importcert \ - -alias $CLIENT_SELF_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -# -# echo -# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" -# keytool \ -# -exportcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $SERVER_STORE \ -# -storepass $SERVER_STORE_PWD | -# keytool \ -# -importcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $CLIENT_STORE \ -# -storepass $CLIENT_STORE_PWD \ -# -noprompt -# - - echo - echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" - keytool \ - -certreq \ - -alias $CLIENT_ALIAS \ - -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -} - -if [ "$client_start" -lt "$client_finish" ]; then - - echo - echo "==Start Client==" - while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do - echo "number $CLIENT_NUMBER" - echo "finish $client_finish" - CLIENT_CN=$(end_point) - CLIENT_ALIAS=$(client_alias_point) - CLIENT_SELF_ALIAS=$(client_self_alias_point) - echo "CLIENT_CN $CLIENT_CN" - echo "CLIENT_ALIAS $CLIENT_ALIAS" - echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" - cert_end_point - CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) - echo - done -fi - -echo -echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" -keytool \ - -importkeystore \ - -srckeystore $SERVER_STORE \ - -destkeystore $SERVER_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" - keytool \ - -importkeystore \ - -srckeystore $CLIENT_STORE \ - -destkeystore $CLIENT_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $CLIENT_STORE_PWD -fi diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties deleted file mode 100644 index 7b3cd9c09a..0000000000 --- a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright © 2016-2017 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. -# - -# Keystore common parameters -ROOT_KEY_ALIAS=rootCA -DOMAIN_SUFFIX="$(hostname)" -ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" -ORGANIZATIONAL_UNIT=Thingsboard -ORGANIZATION=Thingsboard -CITY=SF -STATE_OR_PROVINCE=CA -TWO_LETTER_COUNTRY_CODE=US -VALIDITY=36500 #days -STORETYPE="JKS" - -#Server -SERVER_STORE=serverKeyStore1.jks -SERVER_STORE_PWD=server_ks_password1 -SERVER_ALIAS=server1 -SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" -SERVER_SELF_ALIAS=server_self_signed -SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" -BOOTSTRAP_ALIAS=bootstrap1 -BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" -BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed -BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" - -# Client -CLIENT_STORE=clientKeyStore1.jks -CLIENT_STORE_PWD=client_ks_password1 -CLIENT_ALIAS_PREFIX=client_alias_1 -CLIENT_PREFIX=LwX509___ -CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 -CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" - -# Color output stuff -red=`tput setaf 1` -green=`tput setaf 2` -blue=`tput setaf 4` -bold=`tput bold` -H0=${red}${bold} -H1=${green}${bold} -H2=${blue} -RESET=`tput sgr0` diff --git a/transport/lwm2m/src/main/data/lwm2mserver.jks b/transport/lwm2m/src/main/data/lwm2mserver.jks deleted file mode 100644 index 301f4e2c3ab90931eb166859e71b2c443fa35660..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4017 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 diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 631968ad35..d7513bdf1f 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -114,7 +114,7 @@ transport: # Server X509 Certificates support credentials: # Whether to enable LWM2M server X509 Certificate/RPK support - enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -150,7 +150,7 @@ transport: # Bootstrap server X509 Certificates support credentials: # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support - enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -177,19 +177,19 @@ transport: # X509 trust certificates trust-credentials: # Whether to load X509 trust certificates - enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}" # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" # PEM certificates pem: # Path to the certificates file (holds trust certificates) - cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: # Type of the key store type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates - store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" # Password used to access the key store store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" From ff56e0e6220e349515f929159503f49e3695ef9f Mon Sep 17 00:00:00 2001 From: van-vanich Date: Thu, 30 Dec 2021 17:48:51 +0200 Subject: [PATCH 038/403] realize solve problems for issue for with saving last ts --- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 4 ++-- .../engine/telemetry/TbMsgTimeseriesNodeConfiguration.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 1d52b7cdfa..711b7c1558 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -49,7 +49,7 @@ import java.util.concurrent.TimeUnit; "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' timestamp will be applied. " + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.", uiResources = {"static/rulenode/rulenode-core-config.js"}, - configDirective = "tbActionNodeTimeseriesConfig", + configDirective = "tbActionNodeTimeseriesConfig!", icon = "file_upload" ) public class TbMsgTimeseriesNode implements TbNode { @@ -77,7 +77,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = getTs(msg); + long ts = config.isSaveWithMsgTs() ? msg.getTs() : getTs(msg); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index bb661df905..89abac99fe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,12 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Fri, 31 Dec 2021 13:20:40 +0200 Subject: [PATCH 039/403] UI: Fixed help link for OTA updates --- ui-ngx/src/app/shared/models/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9bf120f8b7..9b1b110a84 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -124,7 +124,7 @@ export const HelpLinks = { rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', resources: helpBaseUrl + '/docs/user-guide/ui/resources', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', - otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates', + otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates', widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration', widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries', From 443bb2280fc62f0fbb4ba023d869e2855b7c233f Mon Sep 17 00:00:00 2001 From: desoliture Date: Fri, 31 Dec 2021 13:27:52 +0200 Subject: [PATCH 040/403] add corresponding test --- .../server/msa/AbstractContainerTest.java | 2 + .../connectivity/MqttGatewayClientTest.java | 71 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index 412cf3094b..e7dc6f2c44 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; import org.apache.cassandra.cql3.Json; import org.apache.commons.lang3.RandomStringUtils; @@ -69,6 +70,7 @@ public abstract class AbstractContainerTest { protected static String TB_TOKEN; protected static RestClient restClient; protected ObjectMapper mapper = new ObjectMapper(); + protected JsonParser jsonParser = new JsonParser(); @BeforeClass public static void before() throws Exception { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 4e7b7cd05b..dfdc68d9d5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -20,6 +20,7 @@ import com.google.common.collect.Sets; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.netty.buffer.ByteBuf; @@ -151,6 +152,76 @@ public class MqttGatewayClientTest extends AbstractContainerTest { Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73))); } + @Test + public void responseDataOnAttributesRequestCheck() throws Exception { + Optional createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId()); + Assert.assertTrue(createdDeviceCredentials.isPresent()); + WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); + JsonObject sharedAttributes = new JsonObject(); + sharedAttributes.addProperty("attr1", "value1"); + sharedAttributes.addProperty("attr2", true); + sharedAttributes.addProperty("attr3", 42.0); + sharedAttributes.addProperty("attr4", 73); + + mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); + ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() + .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", + mapper.readTree(sharedAttributes.toString()), ResponseEntity.class, + createdDevice.getId()); + Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful()); + var event = listener.getEvents().poll(10, TimeUnit.SECONDS); + + JsonObject requestData = new JsonObject(); + requestData.addProperty("id", 1); + requestData.addProperty("device", createdDevice.getName()); + requestData.addProperty("client", false); + requestData.addProperty("key", "attr1"); + + mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); + mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(requestData.toString().getBytes())).get(); + event = listener.getEvents().poll(10, TimeUnit.SECONDS); + + JsonObject responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); + Assert.assertTrue(responseData.has("value")); + Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("value").getAsString()); + + requestData = new JsonObject(); + requestData.addProperty("id", 1); + requestData.addProperty("device", createdDevice.getName()); + requestData.addProperty("client", false); + JsonArray keys = new JsonArray(); + keys.add("attr1"); + keys.add("attr2"); + requestData.add("keys", keys); + + mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); + mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(requestData.toString().getBytes())).get(); + event = listener.getEvents().poll(10, TimeUnit.SECONDS); + responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); + + Assert.assertTrue(responseData.has("values")); + Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); + Assert.assertEquals(sharedAttributes.get("attr2").getAsString(), responseData.get("values").getAsJsonObject().get("attr2").getAsString()); + + requestData = new JsonObject(); + requestData.addProperty("id", 1); + requestData.addProperty("device", createdDevice.getName()); + requestData.addProperty("client", false); + keys = new JsonArray(); + keys.add("attr1"); + keys.add("undefined"); + requestData.add("keys", keys); + + mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); + mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(requestData.toString().getBytes())).get(); + event = listener.getEvents().poll(10, TimeUnit.SECONDS); + responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject(); + + Assert.assertTrue(responseData.has("values")); + Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString()); + Assert.assertEquals(1, responseData.get("values").getAsJsonObject().entrySet().size()); + } + @Test public void requestAttributeValuesFromServer() throws Exception { WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); From 8ec7fd5de8ce0777f15ab9d4ad461e868aaeb50e Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 3 Jan 2022 15:38:40 +0200 Subject: [PATCH 041/403] Removed unused relation dao code --- .../dao/sql/relation/JpaRelationDao.java | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index fbedef6c4e..a7a1ca6a27 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -203,29 +203,4 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit) { return DaoUtil.convertDataList(relationRepository.findRuleNodeToRuleChainRelations(ruleChainType, PageRequest.of(0, limit))); } - - private Specification getEntityFieldsSpec(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType) { - return (root, criteriaQuery, criteriaBuilder) -> { - List predicates = new ArrayList<>(); - if (from != null) { - Predicate fromIdPredicate = criteriaBuilder.equal(root.get("fromId"), from.getId()); - predicates.add(fromIdPredicate); - Predicate fromEntityTypePredicate = criteriaBuilder.equal(root.get("fromType"), from.getEntityType().name()); - predicates.add(fromEntityTypePredicate); - } - if (relationType != null) { - Predicate relationTypePredicate = criteriaBuilder.equal(root.get("relationType"), relationType); - predicates.add(relationTypePredicate); - } - if (typeGroup != null) { - Predicate typeGroupPredicate = criteriaBuilder.equal(root.get("relationTypeGroup"), typeGroup.name()); - predicates.add(typeGroupPredicate); - } - if (childType != null) { - Predicate childTypePredicate = criteriaBuilder.equal(root.get("toType"), childType.name()); - predicates.add(childTypePredicate); - } - return criteriaBuilder.and(predicates.toArray(new Predicate[0])); - }; - } } From bbab11d5aa2d01a4d11a5d92bdefc3047ea5466f Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 4 Jan 2022 13:03:26 +0200 Subject: [PATCH 042/403] UI: Fixed cancel event on color picker --- .../shared/components/dialog/color-picker-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 499f857df8..2374749cbc 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -67,7 +67,7 @@ export class ColorPickerDialogComponent extends DialogComponent Date: Wed, 5 Jan 2022 13:58:58 +0200 Subject: [PATCH 043/403] add test for sequential timeseries persistence and improve code for TbMsgTimeseriesNode rule node and config --- .../server/service/ServiceSqlTestSuite.java | 1 + .../SequentialTimeseriesPersistenceTest.java | 194 ++++++++++++++++++ .../engine/telemetry/TbMsgTimeseriesNode.java | 6 +- .../TbMsgTimeseriesNodeConfiguration.java | 4 +- 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java index 508272085c..d3ad80f614 100644 --- a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java @@ -23,6 +23,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ "org.thingsboard.server.service.resource.sql.*Test", + "org.thingsboard.server.service.sql.*Test" }) public class ServiceSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java new file mode 100644 index 0000000000..9e6b6f0bc1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -0,0 +1,194 @@ +package org.thingsboard.server.service.sql; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@Slf4j +public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest { + + static final int TIMEOUT = 30; + + final String TOTALIZER = "Totalizer"; + final int TTL = 99999; + final String GENERIC_CUMULATIVE_OBJ = "genericCumulativeObj"; + final List ts = List.of(10L, 20L, 30L, 40L, 60L, 70L, 50L, 80L); + final List msgValue = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L); + + @Autowired + TimeseriesService timeseriesService; + + TbMsgTimeseriesNodeConfiguration configuration; + Tenant savedTenant; + User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + configuration = new TbMsgTimeseriesNodeConfiguration(); + configuration.setIgnoreMetadataTs(true); + + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); + } + + @Test + public void testSequentialTimeseriesPersistence() throws Exception { + Asset asset = saveAsset("Asset"); + + Device deviceA = saveDevice("Device A"); + Device deviceB = saveDevice("Device B"); + Device deviceC = saveDevice("Device C"); + Device deviceD = saveDevice("Device D"); + List devices = List.of(deviceA, deviceB, deviceC, deviceD); + + for (int i = 0; i < 2; i++) { + int idx = i * (devices.size()); + saveLatestTsForAssetAndDevice(devices, asset, idx); + checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset); + } + } + + Device saveDevice(String name) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + Assert.assertNotNull(savedDevice); + return savedDevice; + } + + Asset saveAsset(String name) throws Exception { + Asset asset = new Asset(); + asset.setName(name); + asset.setType("default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + Assert.assertNotNull(savedAsset); + return savedAsset; + } + + private void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { + for (Device device : devices) { + TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), + device.getId(), + getTbMsgMetadata(device.getName(), ts.get(idx)), + TbMsgDataType.JSON, + getTbMsgData(msgValue.get(idx))); + saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx)); + saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs())); + idx++; + } + } + + void checkDiffBetweenLatestTsForDevicesAndAsset(List devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); + Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); + JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); + for (Device device : devices) { + Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); + TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); + Assert.assertTrue(deviceLatest.getLongValue().isPresent()); + Long deviceValue = deviceLatest.getLongValue().get(); + Assert.assertEquals(assetValue, deviceValue); + } + } + + String getTbMsgData(long value) { + return "{\"Totalizer\": " + value + "}"; + } + + TbMsgMetaData getTbMsgMetadata(String name, long ts) { + Map metadata = new HashMap<>(); + metadata.put("deviceName", name); + metadata.put("ts", String.valueOf(ts)); + return new TbMsgMetaData(metadata); + } + + void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs()), new LongDataEntry(TOTALIZER, value)); + saveTimeseries(entityId, tsKvEntry); + } + + void saveAssetTsEntry(Asset asset, String key, long value, long ts) throws ExecutionException, InterruptedException, TimeoutException { + Optional tsKvEntryOpt = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ).getJsonValue(); + TsKvEntry saveTsKvEntry = new BasicTsKvEntry(ts, new JsonDataEntry(GENERIC_CUMULATIVE_OBJ, getJsonObject(key, value, tsKvEntryOpt).toString())); + saveTimeseries(asset.getId(), saveTsKvEntry); + } + + @NotNull + private JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { + JsonObject jsonObject = new JsonObject(); + if (tsKvEntryOpt.isPresent()) { + jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); + } + jsonObject.addProperty(key, value); + return jsonObject; + } + + private void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { + timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS); + } + + TsKvEntry getTsKvLatest(EntityId entityId, String key) throws InterruptedException, ExecutionException, TimeoutException { + List tsKvEntries = timeseriesService.findLatest( + savedTenant.getTenantId(), + entityId, + List.of(key)).get(TIMEOUT, TimeUnit.SECONDS); + Assert.assertEquals(1, tsKvEntries.size()); + return tsKvEntries.get(0); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 711b7c1558..47cf0f1bf2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -77,7 +77,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = config.isSaveWithMsgTs() ? msg.getTs() : getTs(msg); + long ts = computeTs(msg, config.isIgnoreMetadataTs()); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { @@ -102,6 +102,10 @@ public class TbMsgTimeseriesNode implements TbNode { } } + public static long computeTs(TbMsg msg, boolean saveWithMsgTs) { + return saveWithMsgTs ? System.currentTimeMillis() : getTs(msg); + } + public static long getTs(TbMsg msg) { long ts = -1; String tsStr = msg.getMetaData().getValue("ts"); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index 89abac99fe..6c9f7ac884 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,14 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Wed, 5 Jan 2022 14:48:45 +0200 Subject: [PATCH 044/403] improve node details and add license for test class --- .../sql/SequentialTimeseriesPersistenceTest.java | 15 +++++++++++++++ .../engine/telemetry/TbMsgTimeseriesNode.java | 15 ++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index 9e6b6f0bc1..c360adf509 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.service.sql; import com.google.gson.JsonObject; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 47cf0f1bf2..2334851a08 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -46,10 +46,15 @@ import java.util.concurrent.TimeUnit; configClazz = TbMsgTimeseriesNodeConfiguration.class, nodeDescription = "Saves timeseries data", nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + - "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' timestamp will be applied. " + - "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.", + "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " + + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " + + "Enable 'ignoreMetadataTs' param to ignore the timestamp that arrives from message metadata. " + + "Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" + + "For example, if you count number of messages from multiple devices into asset time-series value. " + + "Typically, you fetch the previous value of the counter, increment it and then save the value. " + + "If you use timestamp of the original message, the value may be ignored, since it has outdated timestamp comparing to the previous message.", uiResources = {"static/rulenode/rulenode-core-config.js"}, - configDirective = "tbActionNodeTimeseriesConfig!", + configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload" ) public class TbMsgTimeseriesNode implements TbNode { @@ -102,8 +107,8 @@ public class TbMsgTimeseriesNode implements TbNode { } } - public static long computeTs(TbMsg msg, boolean saveWithMsgTs) { - return saveWithMsgTs ? System.currentTimeMillis() : getTs(msg); + public static long computeTs(TbMsg msg, boolean ignoreMetadataTs) { + return ignoreMetadataTs ? System.currentTimeMillis() : getTs(msg); } public static long getTs(TbMsg msg) { From 93bf95c17a63cf5e40e898172fb2a91d3f8ce28b Mon Sep 17 00:00:00 2001 From: van-vanich Date: Wed, 5 Jan 2022 15:00:13 +0200 Subject: [PATCH 045/403] improve code style --- .../sql/SequentialTimeseriesPersistenceTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index c360adf509..e4fb0318b4 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.sql; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Assert; @@ -55,7 +54,6 @@ import java.util.concurrent.TimeoutException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest -@Slf4j public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest { static final int TIMEOUT = 30; @@ -112,7 +110,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest List devices = List.of(deviceA, deviceB, deviceC, deviceD); for (int i = 0; i < 2; i++) { - int idx = i * (devices.size()); + int idx = i * devices.size(); saveLatestTsForAssetAndDevice(devices, asset, idx); checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset); } @@ -136,7 +134,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest return savedAsset; } - private void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { + void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { for (Device device : devices) { TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), device.getId(), @@ -185,7 +183,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest } @NotNull - private JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { + JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { JsonObject jsonObject = new JsonObject(); if (tsKvEntryOpt.isPresent()) { jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); @@ -194,7 +192,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest return jsonObject; } - private void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { + void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS); } From b64eccf3332a2812a931ef697af44a5463d7f0b7 Mon Sep 17 00:00:00 2001 From: desoliture Date: Wed, 5 Jan 2022 16:24:12 +0200 Subject: [PATCH 046/403] add noXss validation for ruleNodes name --- .../java/org/thingsboard/server/common/data/rule/RuleNode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 8201807808..8904d2821d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @Data @@ -41,6 +42,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl @Length(fieldName = "type") @ApiModelProperty(position = 4, value = "Full Java Class Name of the rule node implementation. ", example = "com.mycompany.iot.rule.engine.ProcessingNode") private String type; + @NoXss @Length(fieldName = "name") @ApiModelProperty(position = 5, value = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") private String name; From ec602248c0cc22da1461e50176fdbca2ebf75519 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 5 Jan 2022 16:28:37 +0200 Subject: [PATCH 047/403] lwm2m validate trust certs --- .../TbLwM2MDtlsCertificateVerifier.java | 50 ++++++++++--------- .../config/ssl/AbstractSslCredentials.java | 26 +++++++--- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java index bae7522c78..babf385bc8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -50,13 +50,22 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; import javax.annotation.PostConstruct; import javax.security.auth.x500.X500Principal; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.CLIENT; @@ -119,8 +128,8 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer TbLwM2MSecurityInfo securityInfo = null; // verify if trust - if (config.getTrustSslCredentials().getTrustedCertificates().length > 0) { - if (verifyIssuer(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { + if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { + if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, CLIENT) : null; } @@ -193,31 +202,26 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer } - private X509Certificate verifyIssuer(X509Certificate certificate, X509Certificate[] certificates) { - String issuerCN = config.getTrustSslCredentials().getValueFromSubjectNameByKey(certificate.getIssuerX500Principal().getName(), "CN"); - if (!StringUtils.isBlank(issuerCN)) { + private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); for (int index = 0; index < certificates.length; ++index) { - X509Certificate trust = certificates[index]; - String trustCN = config.getTrustSslCredentials().getValueFromSubjectNameByKey(trust.getSubjectX500Principal().getName(), "CN"); - if (!StringUtils.isBlank(trustCN) && issuerCN.length() >= trustCN.length() && issuerCN.substring(issuerCN.length()-trustCN.length()).equals(trustCN)) { - if (verifyCertificate(certificate)) { - return certificate; - } + X509Certificate caCert = certificates[index]; + try { + TrustAnchor trustAnchor = new TrustAnchor(caCert, null); + CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + PKIXParameters pkixParams = new PKIXParameters( + Collections.singleton(trustAnchor)); + pkixParams.setRevocationEnabled(false); + if (cpv.validate(cp, pkixParams) != null) return certificate; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { + log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); } } + } catch (CertificateException e) { + log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); } return null; } - - private static boolean verifyCertificate(X509Certificate certificate) { - try { - // date - certificate.checkValidity(); - // Validate X509. - SecurityUtil.certificate.decode(certificate.getEncoded()); - return true; - } catch (Exception e) { - return false; - } - } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java index 01b7242805..3170aea9a2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java @@ -61,7 +61,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { this.keyPasswordArray = keyPassword.toCharArray(); } this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray); - Set trustedCerts = getTrustedCerts(this.keyStore); + Set trustedCerts = getTrustedCerts(this.keyStore, trustsOnly); this.trusts = trustedCerts.toArray(new X509Certificate[0]); if (!trustsOnly) { PrivateKeyEntry privateKeyEntry = null; @@ -179,7 +179,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { return entry; } - private static Set getTrustedCerts(KeyStore ks) { + private static Set getTrustedCerts(KeyStore ks, boolean trustsOnly) { Set set = new HashSet<>(); try { for (Enumeration e = ks.aliases(); e.hasMoreElements(); ) { @@ -187,19 +187,33 @@ public abstract class AbstractSslCredentials implements SslCredentials { if (ks.isCertificateEntry(alias)) { Certificate cert = ks.getCertificate(alias); if (cert instanceof X509Certificate) { - set.add((X509Certificate)cert); + if (trustsOnly) { + // is CA certificate + if (((X509Certificate) cert).getBasicConstraints()>=0) { + set.add((X509Certificate) cert); + } + } else { + set.add((X509Certificate) cert); + } } } else if (ks.isKeyEntry(alias)) { Certificate[] certs = ks.getCertificateChain(alias); if ((certs != null) && (certs.length > 0) && (certs[0] instanceof X509Certificate)) { - set.add((X509Certificate)certs[0]); + if (trustsOnly) { + for (Certificate cert : certs) { + // is CA certificate + if (((X509Certificate) cert).getBasicConstraints()>=0) { + set.add((X509Certificate) cert); + } + } + } else { + set.add((X509Certificate)certs[0]); + } } } } } catch (KeyStoreException ignored) {} return Collections.unmodifiableSet(set); } - - } From e5e79a22f6e5ac0ae48634a77b67a841c61346f8 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 5 Jan 2022 18:11:34 +0200 Subject: [PATCH 048/403] lwm2m tests with credentials RPK and X509 - ignore --- .../security/sql/RpkLwM2MIntegrationTest.java | 2 ++ .../sql/X509_NoTrustLwM2MIntegrationTest.java | 2 ++ .../sql/X509_TrustLwM2MIntegrationTest.java | 2 ++ .../resources/application-test.properties | 34 +++++++++---------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 9e74beaa6b..87ca415bd0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -29,6 +30,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index f55c21dcc2..d1bc813b50 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.common.transport.util.SslUtil; @@ -28,6 +29,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 32d176d598..7c5e48a4b7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -27,6 +28,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 651b00ed67..856745797a 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,20 +1,20 @@ -transport.lwm2m.server.security.credentials.enabled=true -transport.lwm2m.server.security.credentials.type=KEYSTORE -transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.server.security.credentials.keystore.store_password=server -transport.lwm2m.server.security.credentials.keystore.key_alias=server -transport.lwm2m.server.security.credentials.keystore.key_password=server -transport.lwm2m.bootstrap.enabled=false -transport.lwm2m.bootstrap.security.credentials.enabled=true -transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE -transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server -transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server -transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server -transport.lwm2m.security.trust-credentials.enabled=true -transport.lwm2m.security.trust-credentials.type=KEYSTORE -transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.security.trust-credentials.keystore.store_password=server +#transport.lwm2m.server.security.credentials.enabled=true +#transport.lwm2m.server.security.credentials.type=KEYSTORE +#transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.server.security.credentials.keystore.store_password=server +#transport.lwm2m.server.security.credentials.keystore.key_alias=server +#transport.lwm2m.server.security.credentials.keystore.key_password=server +#transport.lwm2m.bootstrap.enabled=false +#transport.lwm2m.bootstrap.security.credentials.enabled=true +#transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE +#transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server +#transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server +#transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server +#transport.lwm2m.security.trust-credentials.enabled=true +#transport.lwm2m.security.trust-credentials.type=KEYSTORE +#transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.security.trust-credentials.keystore.store_password=server edges.enabled=true edges.storage.no_read_records_sleep=500 From 659d8abf35fa6f41024f70f299b88e96ce96e302 Mon Sep 17 00:00:00 2001 From: van-vanich Date: Wed, 5 Jan 2022 18:15:16 +0200 Subject: [PATCH 049/403] improve node details and improve field naming --- .../sql/SequentialTimeseriesPersistenceTest.java | 6 +++--- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 13 ++++++++----- .../telemetry/TbMsgTimeseriesNodeConfiguration.java | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index e4fb0318b4..e0a0260f7f 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -74,7 +74,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest @Before public void beforeTest() throws Exception { configuration = new TbMsgTimeseriesNodeConfiguration(); - configuration.setIgnoreMetadataTs(true); + configuration.setUseServerTs(true); loginSysAdmin(); @@ -142,7 +142,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest TbMsgDataType.JSON, getTbMsgData(msgValue.get(idx))); saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx)); - saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs())); + saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs())); idx++; } } @@ -172,7 +172,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest } void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException { - TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs()), new LongDataEntry(TOTALIZER, value)); + TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs()), new LongDataEntry(TOTALIZER, value)); saveTimeseries(entityId, tsKvEntry); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 2334851a08..2c426dfdec 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -48,11 +48,14 @@ import java.util.concurrent.TimeUnit; nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " + - "Enable 'ignoreMetadataTs' param to ignore the timestamp that arrives from message metadata. " + + "
" + + "Enable 'useServerTs' param to use the timestamp of the message processing instead of the timestamp from the message. " + "Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" + - "For example, if you count number of messages from multiple devices into asset time-series value. " + - "Typically, you fetch the previous value of the counter, increment it and then save the value. " + - "If you use timestamp of the original message, the value may be ignored, since it has outdated timestamp comparing to the previous message.", + "
" + + "In the case of sequential processing, the platform guarantees that the messages are processed in the order of their submission to the queue. " + + "However, the timestamp of the messages originated by multiple devices/servers may be unsynchronized long before they are pushed to the queue. " + + "The DB layer has certain optimizations to ignore the updates of the \"attributes\" and \"latest values\" tables if the new record has a timestamp that is older than the previous record. " + + "So, to make sure that all the messages will be processed correctly, one should enable this parameter for sequential message processing scenarios.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload" @@ -82,7 +85,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = computeTs(msg, config.isIgnoreMetadataTs()); + long ts = computeTs(msg, config.isUseServerTs()); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index 6c9f7ac884..0d59c325fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,14 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Wed, 5 Jan 2022 17:47:01 +0200 Subject: [PATCH 050/403] lwm2m add to test shell credentials --- ...cfssl_chain_trusts_and_clients_for_test.sh | 299 ++++++++++++++++++ .../shell/lwm2m_cfssl_chain_for_test_All.sh | 65 ++++ .../lwm2m_cfssl_chain_server_for_test.sh | 298 +++++++++++++++++ 3 files changed, 662 insertions(+) create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh b/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh new file mode 100755 index 0000000000..26f47266a7 --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash + +# Change working directory +cd -- "$( + dirname "${0}" +)" || exit 1 + +readonly TRUST_PATH="Trust" +readonly CA_ROOT_CERT_KEY="ca-root" +readonly CA_ROOT_ALIAS="root" +readonly CA_INTERMEDIATE_CERT_KEY_PREF="intermediate_ca" +CA_INTERMEDIATE_START=0 +CA_INTERMEDIATE_FINISH=2 +CA_INTERMEDIATE_NUMBER=${CA_INTERMEDIATE_START} +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_LIST_CERT_FOR_CAT="" +readonly CA_TRUST_STORE_ALL_CHAIN="lwm2mtruststorechain" +readonly CA_TRUST_STORE_PWD="server_ks_password" +readonly CA_TRUST_CERT_ALIAS="root" +readonly CA_TRUST_CERT_CHAIN_JKS="lwm2mtruststorechain" +readonly CA_TRUST_STORE_CHAIN_ALIAS="trust_cert_chain_alias" + +readonly CLIENT_PATH="Client" +readonly CLIENT_JKS_FOR_TEST="lwm2mclient" +readonly CLIENT_CERT_KEY_PREF="LwX509" +readonly CLIENT_CERT_ALIAS_PREF="client_alias_" +readonly CLIENT_STORE_PWD="client_ks_password" +readonly CLIENT_HOST_NAME="thingsboard_test.io" +CLIENT_START=0 +CLIENT_FINISH=1 +CLIENT_NUMBER=${CLIENT_START} + +SERVER_HOST_NAME="localhost.localdomain" +SERVER_LOCAL_HOST_NAME="localhost" +SERVER_PUBLIC_HOST_NAMES="-" + +readonly CF_COMMANDS=" + cfssl + cfssljson +" + +if [ ! -z "$1" ]; then + CA_INTERMEDIATE_START=$1 + CA_INTERMEDIATE_NUMBER=${CA_INTERMEDIATE_START} +fi + +if [ ! -z "$2" ]; then + CA_INTERMEDIATE_FINISH=$2 +fi + +if [ ! -z "$3" ]; then + CLIENT_START=$1 + CLIENT_NUMBER=${CLIENT_START} +fi + +if [ ! -z "$4" ]; then + CLIENT_FINISH=$4 +fi + +# Change working directory +rm -rf ${TRUST_PATH} +mkdir -p ${TRUST_PATH} +rm -rf ${CLIENT_PATH} +mkdir -p ${CLIENT_PATH} +cd -- "$( + dirname "${0}" +)" || exit 1 + + +rm *.csr +rm *.p12 +rm *.json +rm *.pem +rm *.jks + +intermediate_common_name() { + echo "${CA_INTERMEDIATE_CERT_KEY_PREF}${CA_INTERMEDIATE_NUMBER}" +} + +set_list_sert_for_cat() { + local first="$1" + echo "$first ${CA_LIST_CERT_FOR_CAT}" +} + +client_common_name() { + echo "${CLIENT_CERT_KEY_PREF}$(printf "%08d" ${CLIENT_NUMBER})" +} + +client_alias_name() { + echo "${CLIENT_CERT_ALIAS_PREF}$(printf "%08d" ${CLIENT_NUMBER})" +} + +for COMMAND in ${CF_COMMANDS}; do + if ! command -v ${COMMAND} &> /dev/null; then + echo "ERROR: Missing command ${COMMAND}" >&2 + echo "Install the package from: https://pkg.cfssl.org/" >&2 + exit 1 + fi +done + +tee ./${TRUST_PATH}/ca-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "8760h", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + }, + "profiles": { + "server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "client-server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } + } +} +CONFIG + +tee ./${TRUST_PATH}/ca-root-to-intermediate-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "43800h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 0, + "max_path_len_zero": true + }, + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "digital signature", + "cert sign", + "crl sign", + "signing" + ] + } + } +} +CONFIG + +echo "====================================================" +echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + +while [[ ${CA_INTERMEDIATE_NUMBER} -lt ${CA_INTERMEDIATE_FINISH} ]]; +do + CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) + CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${TRUST_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY} + { + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + } +CONFIG + #openssl x509 -in ${CA_INTERMEDIATE_CERT_KEY}.pem -text -noout + CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +done + +echo "====================================================" +echo -e "Add the CA_certificate to keystore: ${CA_TRUST_CERT_CHAIN_JKS}.jks" +echo "====================================================" +cat ${CA_LIST_CERT_FOR_CAT} > ./${TRUST_PATH}/${CA_TRUST_STORE_ALL_CHAIN}.pem +openssl pkcs12 -export -in ./${TRUST_PATH}/${CA_TRUST_STORE_ALL_CHAIN}.pem -inkey ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem -out ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.p12 -name ${CA_TRUST_STORE_CHAIN_ALIAS} -CAfile ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${CA_TRUST_STORE_PWD} -passout pass:${CA_TRUST_STORE_PWD} +keytool -importkeystore -deststorepass ${CA_TRUST_STORE_PWD} -destkeypass ${CA_TRUST_STORE_PWD} -destkeystore ./${TRUST_PATH}/${CA_TRUST_CERT_CHAIN_JKS}.jks -srckeystore ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CA_TRUST_STORE_PWD} -alias ${CA_TRUST_STORE_CHAIN_ALIAS} + +keytool -list -v -keystore ./${TRUST_PATH}/lwm2mtruststorechain.jks -storepass server_ks_password -storetype PKCS12 + +echo "====================================================" +echo -e "Generate and Signed the clients of our certificates: \n-${CLIENT_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CLIENT_CERT_KEY_PREF}?.pem (certificate)\n-${CCLIENT_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + + +while [[ ${CLIENT_NUMBER} -lt ${CLIENT_FINISH} ]]; +do + CLIENT_CERT_KEY=$(client_common_name) + CLIENT_CERT_ALIAS=$(client_alias_name) + CLIENT_NUMBER=$((${CLIENT_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${TRUST_PATH}/ca-config.json \ + -profile client \ + -hostname "${CLIENT_HOST_NAME}" \ + - \ + <<-CONFIG | cfssljson -bare ./${CLIENT_PATH}/${CLIENT_CERT_KEY} +{ + "CN": "${CLIENT_CERT_KEY}" +} +CONFIG + +echo "====================================================" +echo -e "Add the client certificate (${CLIENT_CERT_KEY}.pem) to keystore: ${CLIENT_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${CLIENT_PATH}/${CLIENT_CERT_KEY}_chain.pem +openssl pkcs12 -export -in ./${CLIENT_PATH}/${CLIENT_CERT_KEY}_chain.pem -inkey ./${CLIENT_PATH}/${CLIENT_CERT_KEY}-key.pem -out ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.p12 -name ${CLIENT_CERT_ALIAS} -CAfile ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${CLIENT_STORE_PWD} -passout pass:${CLIENT_STORE_PWD} +keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT_STORE_PWD} -destkeystore ./${CLIENT_PATH}/${CLIENT_JKS_FOR_TEST}.jks -srckeystore ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CLIENT_STORE_PWD} -alias ${CLIENT_CERT_ALIAS} + +done + +keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 + +rm ./${TRUST_PATH}/*.p12 +rm ./${TRUST_PATH}/*.csr +rm ./${TRUST_PATH}/*.json +rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* +rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* + + +rm ./${CLIENT_PATH}/*.p12 2> /dev/null +rm ./${CLIENT_PATH}/*.csr 2> /dev/null diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh new file mode 100755 index 0000000000..b3b114cb28 --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +readonly INTERMEDIATE_START=0 +readonly INTERMEDIATE_FINISH=2 +readonly CLIENT_START=0 +readonly CLIENT_FINISH=5 + +IS_IHFO=false +IS_SERVER_CREATED_KEY=true +IS_TRUST_CLIENT_CREATED_KEY=true + +cd -- "$( + dirname "${0}" +)" || exit 1 + +Help() +{ + # Display Help + echo "Description of the script functions." + echo + echo "Syntax: scriptTemplate [-g|h|v|V]" + echo "options:" + echo "h Print this Help." + echo "v Verbose mode." + echo "V Print software version and exit." + echo +} + +if [ "$1" == "-h" ] ; then + echo -e "Usage 2: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 1: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 3: ./`basename $0` true false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 4: ./`basename $0` true false false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are not generated\"" + echo -e "Usage 4: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" + echo "This Help File: ./`basename $0` -h" + exit 0 +fi + +if [ -n "$1" ]; then + IS_IHFO=$1 +fi + +if [ -n "$2" ]; then + IS_SERVER_CREATED_KEY=$2 +fi + +if [ -n "$3" ]; then + IS_TRUST_CLIENT_CREATED_KEY=$3 +fi + +if [ "$IS_IHFO" = false ] ; then + if [ "$IS_SERVER_CREATED_KEY" = true ] ; then + ./lwm2m_cfssl_chain_server_for_test.sh > /dev/null 2>&1 & + fi + if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then + ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & + fi +else + if [ "$IS_SERVER_CREATED_KEY" = true ] ; then + ./lwm2m_cfssl_chain_server_for_test.sh + fi + if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then + ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} + fi +fi \ No newline at end of file diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh new file mode 100755 index 0000000000..efe6ed46dd --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash + +# REF: https://github.com/cloudflare/cfssl + +# Change working directory +cd -- "$( + dirname "${0}" +)" || exit 1 + +readonly CA_ROOT_CERT_KEY="ca-root" +readonly CA_ROOT_ALIAS="root" +readonly CA_INTERMEDIATE_CERT_KEY_PREF="intermediate_ca" +CA_INTERMEDIATE_NUMBER=0 +CA_LIST_CERT_FOR_CAT="" + +readonly CF_COMMANDS=" + cfssl + cfssljson +" + +readonly SERVER_JKS_FOR_TEST="lwm2mserver" +readonly STORE_PASS_PWD="server_ks_password" +readonly SERVER_PATH="Server" +readonly SERVER_CERT_KEY="lwm2mserver" +readonly SERVER_CERT_CHAIN="lwm2mserver_chain" +readonly SERVER_CERT_ALIAS="server" +readonly BS_SERVER_CERT_KEY="lwm2mserverbs" +readonly BS_SERVER_CERT_CHAIN="lwm2mserverbs_chain" +readonly BS_SERVER_CERT_ALIAS="bootstrap" + +SERVER_HOST_NAME="localhost.localdomain" +SERVER_LOCAL_HOST_NAME="localhost" +SERVER_PUBLIC_HOST_NAMES="-" + +intermediate_common_name() { + echo "${CA_INTERMEDIATE_CERT_KEY_PREF}${CA_INTERMEDIATE_NUMBER}" +} + +set_list_sert_for_cat() { + local first="$1" + echo "$first ${CA_LIST_CERT_FOR_CAT}" +} + + +# Change working directory +rm -rf ${SERVER_PATH} +mkdir -p ${SERVER_PATH} + +cd -- "$( + dirname ./${SERVER_PATH} +)" || exit 1 + + +rm *.csr +rm *.p12 +rm *.json +rm *.pem +rm *.jks + +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) +CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) +CA_LIST_CERT_FOR_CAT="" + +for COMMAND in ${CF_COMMANDS}; do + if ! command -v ${COMMAND} &> /dev/null; then + echo "ERROR: Missing command ${COMMAND}" >&2 + echo "Install the package from: https://pkg.cfssl.org/" >&2 + exit 1 + fi +done + +tee ./${SERVER_PATH}/ca-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "8760h", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + }, + "profiles": { + "server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "client-server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } + } +} +CONFIG + +tee ./${SERVER_PATH}/ca-root-to-intermediate-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "43800h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 0, + "max_path_len_zero": true + }, + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "digital signature", + "cert sign", + "crl sign", + "signing" + ] + } + } +} +CONFIG + +echo "====================================================" +echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA for servers", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the first intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY}-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY}.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${SERVER_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY} +{ + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + + +## Lwm2m Server certificate +echo "====================================================" +echo -e "Generate and Signed the server certificate: \n-${SERVER_CERT_KEY}-key.pem (certificate key)\n-${SERVER_CERT_KEY}.pem (certificate)\n-${SERVER_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${SERVER_PATH}/ca-config.json \ + -profile server \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${SERVER_CERT_KEY} +{ + "CN": "${SERVER_LOCAL_HOST_NAME}" +} +CONFIG + +echo "====================================================" +echo -e "Add the server certificate (${SERVER_CERT_KEY}.pem) to keystore: ${SERVER_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${SERVER_PATH}/${SERVER_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${SERVER_PATH}/${SERVER_CERT_CHAIN}.pem +openssl pkcs12 -export -in ./${SERVER_PATH}/${SERVER_CERT_CHAIN}.pem -inkey ./${SERVER_PATH}/${SERVER_CERT_KEY}-key.pem -out ./${SERVER_PATH}/${SERVER_CERT_KEY}.p12 -name ${SERVER_CERT_ALIAS} -CAfile ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${STORE_PASS_PWD} -passout pass:${STORE_PASS_PWD} +keytool -importkeystore -deststorepass ${STORE_PASS_PWD} -destkeypass ${STORE_PASS_PWD} -destkeystore ./${SERVER_PATH}/${SERVER_JKS_FOR_TEST}.jks -srckeystore ./${SERVER_PATH}/${SERVER_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${STORE_PASS_PWD} -alias ${SERVER_CERT_ALIAS} + + +CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) +CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) +echo "====================================================" +echo -e "Generate and Signed the second intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY}-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY}.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${SERVER_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY} +{ + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + +## Bootstrap server certificate +echo "====================================================" +echo -e "Generate and Signed the server certificate: \n-${BS_SERVER_CERT_KEY}-key.pem (certificate key)\n-${BS_SERVER_CERT_KEY}.pem (certificate)\n-${BS_SERVER_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${SERVER_PATH}/ca-config.json \ + -profile server \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${BS_SERVER_CERT_KEY} +{ + "CN": "${SERVER_LOCAL_HOST_NAME}" +} +CONFIG + +echo "====================================================" +echo -e "Add the Bootstrap server certificate (${BS_SERVER_CERT_KEY}.pem) to keystore: ${SERVER_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${SERVER_PATH}/${BS_SERVER_CERT_CHAIN}.pem +openssl pkcs12 -export -in ./${SERVER_PATH}/${BS_SERVER_CERT_CHAIN}.pem -inkey ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}-key.pem -out ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.p12 -name ${BS_SERVER_CERT_ALIAS} -CAfile ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${STORE_PASS_PWD} -passout pass:${STORE_PASS_PWD} +keytool -importkeystore -deststorepass ${STORE_PASS_PWD} -destkeypass ${STORE_PASS_PWD} -destkeystore ./${SERVER_PATH}/${SERVER_JKS_FOR_TEST}.jks -srckeystore ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${STORE_PASS_PWD} -alias ${BS_SERVER_CERT_ALIAS} + + +keytool -list -v -keystore ./${SERVER_PATH}/lwm2mserver.jks -storepass server_ks_password -storetype PKCS12 + +rm ./${SERVER_PATH}/*.p12 2> /dev/null +rm ./${SERVER_PATH}/*.csr 2> /dev/null +rm ./${SERVER_PATH}/*.json 2> /dev/null +rm ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* 2> /dev/null +rm ./${SERVER_PATH}/${CA_ROOT_CERT_KEY}* 2> /dev/null +mv ./${SERVER_PATH}/${SERVER_CERT_KEY}-key.pem ./${SERVER_PATH}/${SERVER_CERT_KEY}_key.pem +mv ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}-key.pem ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}_key.pem + From 60758375ed02f06e06cce51c31ccf5171bd7c8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20Karda=C5=9Flar?= Date: Sat, 8 Jan 2022 01:03:54 +0300 Subject: [PATCH 051/403] fix problematic letters for different lang --- .../org/thingsboard/server/dao/audit/AuditLogLevelFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java index b73cafc121..effc79f711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import java.util.HashMap; +import java.util.Locale; import java.util.Map; public class AuditLogLevelFilter { @@ -28,7 +29,7 @@ public class AuditLogLevelFilter { public AuditLogLevelFilter(Map mask) { entityTypeMask.clear(); mask.forEach((entityTypeStr, logLevelMaskStr) -> { - EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase()); + EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase(Locale.ENGLISH)); AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase()); entityTypeMask.put(entityType, logLevelMask); }); From eccbd3290c5c443ae60fae111ce9539d28a6e85d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 13:06:46 +0200 Subject: [PATCH 052/403] lwm2m tests with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../AbstractSecurityLwM2MIntegrationTest.java | 198 ++++++++++-------- 1 file changed, 105 insertions(+), 93 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index ea30b43436..0c983f9dcf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.transport.lwm2m.security; -import org.eclipse.leshan.core.util.Hex; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -24,144 +23,157 @@ import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import java.io.IOException; import java.io.InputStream; -import java.math.BigInteger; -import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; -import java.security.KeyFactory; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPrivateKeySpec; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.KeySpec; @DaoSqlTest public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { + protected final String CREDENTIALS_PATH = "lwm2m/credentials/"; // client public key or id used for PSK protected final String pskIdentity; // client public key or id used for PSK protected final String pskKey; // client private/secret key used for PSK - protected final PublicKey clientPublicKey; // client public key used for RPK - protected final PrivateKey clientPrivateKey; // client private key used for RPK - protected final PublicKey serverPublicKey; // server public key used for RPK - protected final PrivateKey serverPrivateKey; // server private key used for RPK - - // client private key used for X509 - protected final PrivateKey clientPrivateKeyFromCert; - // server private key used for X509 - protected final PrivateKey serverPrivateKeyFromCert; - // client certificate signed by rootCA with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509Cert; - // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) - protected final X509Certificate clientX509CertWithBadCN; - // client certificate self-signed with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509CertSelfSigned; - // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509CertNotTrusted; - // server certificate signed by rootCA - protected final X509Certificate serverX509Cert; +// protected final PublicKey clientPublicKey; // client public key used for RPK +// protected final PrivateKey clientPrivateKey; // client private key used for RPK + + + +// // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) +// protected final X509Certificate clientX509CertWithBadCN; +// // client certificate self-signed with a good CN (CN start by leshan_integration_test) +// protected final X509Certificate clientX509CertSelfSigned; +// // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) +// protected final X509Certificate clientX509CertNotTrusted; + // self-signed server certificate - protected final X509Certificate serverX509CertSelfSigned; - // rootCA used by the server - protected final X509Certificate rootCAX509Cert; +// protected final X509Certificate serverX509CertSelfSigned; +// // rootCA used by the server +// protected final X509Certificate rootCAX509Cert; // certificates trustedby the server (should contain rootCA) - protected final Certificate[] trustedCertificates = new Certificate[1]; - protected static final String ENDPOINT = "deviceAEndpoint"; + // Server + protected static final String SERVER_JKS_FOR_TEST = "lwm2mserver"; + protected static final String SERVER_STORE_PWD = "server_ks_password"; + protected static final String SERVER_CERT_ALIAS = "server"; + protected final X509Certificate serverX509Cert; // server certificate signed by rootCA +// protected final PrivateKey serverPrivateKeyFromCert; // server private key used for RPK and X509 + protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK +// // Server Trust +// protected final Certificate[] trustedCertificates = new Certificate[1]; + + // Client protected LwM2MTestClient client; + protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; + protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; + protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; + protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; + protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; + protected static final String CLIENT_STORE_PWD = "client_ks_password"; + + protected static final String CLIENT_CERT_ALIAS = "client_alias_00000000"; + + protected final X509Certificate clientX509Cert; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCert; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCert; // client public key used for RPK + private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; - private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); - setEndpoint(ENDPOINT); + setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { - // Get keys PSK +// Get keys PSK this.pskIdentity = "SOME_PSK_ID"; this.pskKey = "73656372657450534b73656372657450"; - // Get point values - byte[] publicX = Hex - .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); - byte[] publicY = Hex - .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); - byte[] privateS = Hex - .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); - - // Get Elliptic Curve Parameter spec for secp256r1 - AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); - algoParameters.init(new ECGenParameterSpec("secp256r1")); - ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); - - // Create key specs - KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), - parameterSpec); - KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); - - // Get keys RPK - clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); - clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); +// // Get point values +// byte[] publicX = Hex +// .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); +// byte[] publicY = Hex +// .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); +// byte[] privateS = Hex +// .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); +// +// // Get Elliptic Curve Parameter spec for secp256r1 +// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); +// algoParameters.init(new ECGenParameterSpec("secp256r1")); +// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); +// +// // Create key specs +// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), +// parameterSpec); +// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); +// +// // Get keys RPK +// clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); +// clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); // Get certificates from key store - char[] clientKeyStorePwd = "client".toCharArray(); + char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/clientKeyStore.jks")) { + try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + CLIENT_JKS_FOR_TEST + ".jks")) { clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey("client", clientKeyStorePwd); - clientX509Cert = (X509Certificate) clientKeyStore.getCertificate("client"); - clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); - clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); - clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); + clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey(CLIENT_CERT_ALIAS, clientKeyStorePwd); + clientX509Cert = (X509Certificate) clientKeyStore.getCertificate(CLIENT_CERT_ALIAS); + clientPublicKeyFromCert = clientX509Cert.getPublicKey(); + +// clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); +// clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); +// clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } // create server credentials try { - // Get point values - byte[] publicX = Hex - .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); - byte[] publicY = Hex - .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); - byte[] privateS = Hex - .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); - - // Get Elliptic Curve Parameter spec for secp256r1 - AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); - algoParameters.init(new ECGenParameterSpec("secp256r1")); - ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); - - // Create key specs - KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), - parameterSpec); - KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); - - // Get keys - serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); - serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); +// // Get point values +// byte[] publicX = Hex +// .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); +// byte[] publicY = Hex +// .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); +// byte[] privateS = Hex +// .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); +// +// // Get Elliptic Curve Parameter spec for secp256r1 +// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); +// algoParameters.init(new ECGenParameterSpec("secp256r1")); +// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); +// +// // Create key specs +// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), +// parameterSpec); +// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); +// +// // Get keys +// serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); +// serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); + // Get certificates from key store - char[] serverKeyStorePwd = "server".toCharArray(); + char[] serverKeyStorePwd = SERVER_STORE_PWD.toCharArray(); KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (InputStream serverKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/serverKeyStore.jks")) { + try (InputStream serverKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + SERVER_JKS_FOR_TEST + ".jks")) { serverKeyStore.load(serverKeyStoreFile, serverKeyStorePwd); } - serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); - rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); - serverX509Cert = (X509Certificate) serverKeyStore.getCertificate("server"); - serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); - trustedCertificates[0] = serverX509Cert; +// serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); + serverX509Cert = (X509Certificate) serverKeyStore.getCertificate(SERVER_CERT_ALIAS); + serverPublicKeyFromCert = serverX509Cert.getPublicKey(); +// rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); + +// serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); +// trustedCertificates[0] = serverX509Cert; } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } From ba970c5d88eb1a723b12e2d8da7822e79c352554 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 13:07:55 +0200 Subject: [PATCH 053/403] lwm2m tests with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../sql/NoSecLwM2MIntegrationTest.java | 5 +++-- .../security/sql/PskLwm2mIntegrationTest.java | 5 +++-- .../security/sql/RpkLwM2MIntegrationTest.java | 12 ++++++------ .../sql/X509_NoTrustLwM2MIntegrationTest.java | 18 +++++++++--------- .../sql/X509_TrustLwM2MIntegrationTest.java | 6 ++---- .../resources/application-test.properties | 18 +++++++++--------- .../lwm2m/credentials/clientKeyStore.jks | Bin 4810 -> 0 bytes .../lwm2m/credentials/lwm2mclient.jks | Bin 0 -> 17660 bytes .../lwm2m/credentials/lwm2mserver.jks | Bin 0 -> 6432 bytes .../credentials/lwm2mtruststorechain.jks | Bin 0 -> 2982 bytes .../lwm2m/credentials/serverKeyStore.jks | Bin 3806 -> 0 bytes 11 files changed, 32 insertions(+), 32 deletions(-) delete mode 100644 application/src/test/resources/lwm2m/credentials/clientKeyStore.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mclient.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mserver.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks delete mode 100644 application/src/test/resources/lwm2m/credentials/serverKeyStore.jks diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 0e86c6a438..8331b99fff 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import lombok.extern.slf4j.Slf4j; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -28,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(ENDPOINT); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, ENDPOINT); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index af9a668376..5850e52015 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.util.Hex; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -33,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(ENDPOINT); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, ENDPOINT); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 87ca415bd0..e6c26a05d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -34,13 +34,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(ENDPOINT); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKey.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPrivateKeyFromCert.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, - clientPublicKey.getEncoded(), - clientPrivateKey.getEncoded(), - serverX509Cert.getPublicKey().getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, ENDPOINT); + clientPublicKeyFromCert.getEncoded(), + clientPrivateKeyFromCert.getEncoded(), + serverPublicKeyFromCert.getEncoded()); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index d1bc813b50..6ca430327d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -32,15 +32,15 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { - X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(ENDPOINT); - credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); - Security security = x509(SECURE_URI, - SHORT_SERVER_ID, - clientX509CertNotTrusted.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), - serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT); +// X509ClientCredential credentials = new X509ClientCredential(); +// credentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); +// credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); +// Security security = x509(SECURE_URI, +// SHORT_SERVER_ID, +// clientX509CertNotTrusted.getEncoded(), +// clientPrivateKeyNotTrustedFromCert.getEncoded(), +// serverX509Cert.getEncoded()); +// super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 7c5e48a4b7..28a10fd278 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -28,17 +27,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(ENDPOINT); + credentials.setEndpoint(CLIENT_ENDPOINT_TRUST); Security security = x509(SECURE_URI, SHORT_SERVER_ID, clientX509Cert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 856745797a..dd1cd2807c 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,19 +1,19 @@ -#transport.lwm2m.server.security.credentials.enabled=true -#transport.lwm2m.server.security.credentials.type=KEYSTORE -#transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.server.security.credentials.enabled=true +transport.lwm2m.server.security.credentials.type=KEYSTORE +transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks #transport.lwm2m.server.security.credentials.keystore.store_password=server #transport.lwm2m.server.security.credentials.keystore.key_alias=server #transport.lwm2m.server.security.credentials.keystore.key_password=server #transport.lwm2m.bootstrap.enabled=false -#transport.lwm2m.bootstrap.security.credentials.enabled=true -#transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE -#transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.bootstrap.security.credentials.enabled=true +transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE +transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks #transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server #transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server #transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server -#transport.lwm2m.security.trust-credentials.enabled=true -#transport.lwm2m.security.trust-credentials.type=KEYSTORE -#transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.security.trust-credentials.enabled=true +transport.lwm2m.security.trust-credentials.type=KEYSTORE +transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/lwm2mtruststorechain.jks #transport.lwm2m.security.trust-credentials.keystore.store_password=server edges.enabled=true diff --git a/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks b/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks deleted file mode 100644 index a6c9ae7faed05c48ec1218e23a42c3025ae852d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4810 zcmeHKdpwls9)IVZ8?$1V+`8z^r7faoTyo7Kw~~aYeB{y?OqdySK}BLOgxw-rlBDFa z(TZxxaVZgsq;!#Oed=sfBHG$cn>de|>D0_gr#|2u2XxmVxY?(+F|Bkr}Xrw_K2ni-bWfz@s=6CB*GFPk0g!dufX^N|u{P18lHZ>cz+#X=+GmPnADKMv-z)w>@4JBhb z95ADsN(Mgk=M8{HB8%2Ragqh`I5OS=A$$_)db__`Kf&{*+vqJu#2QlLoO2`e+65gI z-$gJeM>`{JIjxqb>8Ge_!lDMXA86YTCXKbnuGg^n=F+^#I@_T_2Ve*0!Y$8LB&ZQ) z8fIy2sm?Jp&$_ZJ{#<&$?-nG%2PjVctc1f&65w0nNI+@oo;Gz)1bBFnJ_Nig0-VtK z$i+`9(rT0kqX*_j_w@*Fd+BSSSe=pDsrHJU_wP4v8bC4krd)?AT1kHN7-kN~$tF(BaxIDJ$Zha)0{i0J#> zovXR|mu?hxEl}b7x^;3Dt#7!s^lHm~yIG-8)Li9+;k)X(YBi1vEBDnnA>Ez!mT$3`Q4`n9W+^X!+{4QYO-S3QT6X}EDl&Ek_+k0!5+ zYpLBXV#z;PQ2-f{2g6>%3gV9>L+Jzg5U|p$u#jbig&&wj*8pl_N-2dfV^|EPy%Rfx z%VDtvV0v2U9!Lqw-7z#C_5+1q>%r{Zrb<@rA-bN~cDcLZR##D3?^iAxJ|7PzzqBvV z*yMKZMJcE6p^Zs|bIl@!0%}5XejyKV-=T%)i-5C@=hRPSl+Ag>Zzi$gZEP1x(*mCg z~3 zcUGX)I%_?*E_{3-ZJa+cD?i<-R#=W=gwmu2hr~i!y5^m%St(XSs=P&_3t+#~|>9H!PwQ{m$UWGO2k zOFy7X*9MqKKnj|XB2NyK>?oEc2$yW@k1ZvUBV)Y!3qJ=m9_lR#FWA#Q!FtBRKlEK@R$tJlo_n3b=vB)WQS;b+(7BZC~gcdc{5>TRX>Hkx@c6+Pa||hi){oR?wITG zsg(^leJ5-T^ZT;2SlX2DLO$JrktE@ie zV(3OUHZe6@YW@KxHl`D?&h_4cZl}NScqNqV38S?a8?N=awxM`xRG?1AJYVaiudOGe zpVML!quB?GvXVcrfBp2AXyKVh6d6+wN(pE9O zsn;F7ks3QNc{q&6s497^$v>WtV0af}+7+|4Ma=vf{-UO`3IbZ2(e6)Sd+W8OGKhnve&F{pX!=}TYj4pS)bm=q zHg>r3!Tw%uPva9JIo&dQB!vMD}d)xMoZa~hgv(AwfIo@}ty=^|8i z?n)v@R27DTkau$Rd!dmn;Z)#ji%U2;K!PP4ijz*-ZqJH)6dp%CKfEigBQr|i7~(ZE zSNc%JWgeC&#P;~!=vb~|O|B&94a$Fi-o|$Jf-!&9>IK^k`kO|fskCT;0hJLwRNGo% zCZ`=e)~M|P>N&lva`&9jLkU_gBIznXQxTUp{5Q4tLBl^m{$qix3_<2fg!XQil~?o@ z=_63e`7j(La*k_O?v!R8_|N5RCM#z%zkh4i=|vq~di=F*tao-9r*Ip2(xUaGp`_g-CJ{w$T`QZdmIwKlWK4#oVF!}tB|TGZ8X?OTd* z)Y^xIg29viIhbnLsgyi}V&_7UU{xeV6q_Rm6!3(60W(Bg+a1{O7R+)8WGRd{wH@(s zK${GhI`sLXb^*4`8j_Y-$d*hvP4oP|D)0ZkP5Rn*Om0!NWp4tgDGpG|L5z=m>HIMn zbp-;Pp~*g28Bx;KSa>euVO`z+-kczeW;a<=?+S=f1MQ;`&`d}d^5AfFY+egkWc!CP39AfyQ}=1VlHt()E_ z1M7$I_@8ZhJ%PtNiS+k2y&uujiVJc5Gsor4Iq{=&2E7sHiH7dql=}VmE~1%wAjS=W z-US%MD#xzn+KIdbuX4}O1#U6Evu~aWyJvEnGFWtBU;QUP+JGyKj%mKTb$|Y4AvgC` M^f%}Gep;{ccMhiIP5=M^ diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks b/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks new file mode 100644 index 0000000000000000000000000000000000000000..490812c01ab483d3462a09cdd18bc405fa23abe5 GIT binary patch literal 17660 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 KeSQK10fwM$Q%GO{ literal 0 HcmV?d00001 diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks b/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks new file mode 100644 index 0000000000000000000000000000000000000000..a1923e942f923b8d84a989055ec50c2441f79259 GIT binary patch literal 6432 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 literal 0 HcmV?d00001 diff --git a/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks b/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks deleted file mode 100644 index fc541a3b18111312545da774be682ddb893d72c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3806 zcmezO_TO6u1_mY|W&_iFnRz9tMY*XdnTaK-$%zaMjFr!(OnJ+|8lh)uUJkHs&Ff*|-7)Tn58;G(ohq5pW^ZBF}XC&q+6y@ia zC^$PBD2VeK85)=wm>U`cNz*6^ej`JmfT^JgRKP&dP~JcmqFD^88QDo7Lm~}?+1SDM zF)>0dW@cn(c4A;T@VS|9lH1dLZ_jKt-`U6Fea+QH`BYo9IL#mhY`ZKUix`Ut^Mt&kdKuN{mQQtB zUi5*fk(H(TpaCDyakBi3jQ?4HzGY-H5CZXqL3|DaHXy~s$Y3B35>sZ8Fc51H(VZk$ zxqXJN`J@Z|H3z%zhgCAvhN1=-vnPXr8zz+tX`Lo>Xmwt;`4ciQ-RUJ$iP{g zT2z)=1Wd({+%)YhP`KWp7MPN_wAmP07`2!L85vm_Sh9C4soL}0b=E$Mg=v$soQ%&E ze-UkGh(7!8*z23GK6Ux3JW7}-CdGRxUb*$s#*H}>yS#3hWc=9SwbZeCr>?;9ccr&_ zYgxQJ(!V{(HT$V`QR>5%H$^P@+qt&id0fzvyI|3@t`COBEccC7VLd&rY7L^CxjgF5%vL_s_{?G7bKy zS)IAVKm?R*1X*|uxSKe0@{<#DGV+T{3^>7wiG|4kHQ!?`JcL1NRatlpxY#&=anH)i z&di7!(9G@(1};nrn!6{v?BTePd(kMjNR;uU^|$ugrFS$Q9umsi${YQ5&H^TdsFeM= z90yik-kWEXGgr=O`{J&$y|KM7LUoxI7hXH8CBN4cMod%AvKSoW?FOfOlv-vGHtp+D%Gs zJc(4Dp!3?mQ2w@XY2U+&4qJBf-UtR0M-HvV>(w&A3ge>joNbbJ!JgUYuAZ6QqPAQv zQ)->$ngpAsU+XTp#rL^@s{=MjR72u+>TO(gfe)|D(-6Qb2h|ya67LSJ{;U~ z=j!t7JN^Y!?~YK;neEaj@$>WA(44J%&My4R*S)D{)LMO8UGTuGsl5-*N3oSb>J z)Lvu@%h8K!V%cK1e3=wtzAp%{I$kwDp0DD0XHV~}Lt>(a&rX)h7S2h_+IP<}98nI8 zw&b9#Z#uT*m>7g0Sr@a2o|KM=nlfPCDFR|qSR>n^QvogiV9x@mC9xTLNo*EnfL$LA zvreFaKLuHbh@1iK`FxVtS7y-vtNrrlO-x^(POD+NzGweDCVTEHScmMzZ From 4450df292f73ce0b58ac52b95bd7a1366f47cbea Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 18:24:10 +0200 Subject: [PATCH 054/403] lwm2m tests aad logs with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../server/transport/lwm2m/AbstractLwM2MIntegrationTest.java | 3 +++ .../lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java | 1 + .../transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java | 4 ++-- application/src/test/resources/application-test.properties | 2 +- application/src/test/resources/logback.xml | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 5d4c32641d..13e0858199 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m; import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.leshan.client.object.Security; @@ -66,6 +67,7 @@ import java.util.concurrent.ScheduledExecutorService; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Slf4j @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { @@ -199,6 +201,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest createNewClient(security, coapConfig, false); String msg = wsClient.waitForUpdate(); + log.info("msg5555: [{}]", msg); EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 0c983f9dcf..d537062def 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -68,6 +68,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; + protected static final String CLIENT_ENDPOINT_PSK = "devicePSK"; protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 5850e52015..d7296fd47c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index dd1cd2807c..7f7cb6e8b9 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -4,7 +4,7 @@ transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credential #transport.lwm2m.server.security.credentials.keystore.store_password=server #transport.lwm2m.server.security.credentials.keystore.key_alias=server #transport.lwm2m.server.security.credentials.keystore.key_password=server -#transport.lwm2m.bootstrap.enabled=false +transport.lwm2m.bootstrap.enabled=false transport.lwm2m.bootstrap.security.credentials.enabled=true transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index d3301bf660..91ca9c2b6c 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,6 +10,7 @@ + From fe9b61586f3f907289d59a9ecca839965f26c07c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 20:11:39 +0200 Subject: [PATCH 055/403] lwm2m tests ignore test del 3/0/9 --- .../sql/RpcLwm2mIntegrationDeleteTest.java | 24 +++++++++---------- application/src/test/resources/logback.xml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index 6f24d7176c..90a9dd7a62 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -74,18 +74,18 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe } - /** - * delete resource - * Delete {"id":"/3/0/9"} - * {"result":"METHOD_NOT_ALLOWED"} - */ - @Test - public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; - String actualResult = sendRPCDeleteById(expectedPath); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); - } +// /** +// * delete resource +// * Delete {"id":"/3/0/9"} +// * {"result":"METHOD_NOT_ALLOWED"} +// */ +// @Test +// public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { +// String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; +// String actualResult = sendRPCDeleteById(expectedPath); +// ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); +// assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); +// } private String sendRPCDeleteById(String path) throws Exception { diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 91ca9c2b6c..4cca303e8f 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,7 @@ - + From 98331c79a0ccb70a4c8459e8bdc56eb92e50c07a Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 23:48:24 +0200 Subject: [PATCH 056/403] lwm2m tests add RPK --- .../transport/lwm2m/Lwm2mTestHelper.java | 1 + .../sql/RpcLwm2mIntegrationDeleteTest.java | 25 ++++++++++--------- .../AbstractSecurityLwM2MIntegrationTest.java | 3 --- .../sql/NoSecLwM2MIntegrationTest.java | 4 +-- .../security/sql/PskLwm2mIntegrationTest.java | 4 +-- .../security/sql/RpkLwM2MIntegrationTest.java | 7 +++--- application/src/test/resources/logback.xml | 1 - 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 232332e554..0ea700d46c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -55,6 +55,7 @@ public class Lwm2mTestHelper { public static final int resourceId_2 = 2; public static final int resourceId_3 = 3; public static final int resourceId_4 = 4; + public static final int resourceId_7 = 7; public static final int resourceId_8 = 8; public static final int resourceId_9 = 9; public static final int resourceId_11 = 11; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index 90a9dd7a62..ebc0f6d783 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_7; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; @@ -74,18 +75,18 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe } -// /** -// * delete resource -// * Delete {"id":"/3/0/9"} -// * {"result":"METHOD_NOT_ALLOWED"} -// */ -// @Test -// public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { -// String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; -// String actualResult = sendRPCDeleteById(expectedPath); -// ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); -// assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); -// } + /** + * delete resource + * Delete {"id":"/3/0/7"} + * {"result":"METHOD_NOT_ALLOWED"} + */ + @Test + public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { + String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_7; + String actualResult = sendRPCDeleteById(expectedPath); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); + } private String sendRPCDeleteById(String path) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index d537062def..b0fadf1d23 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -67,9 +67,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; - protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; - protected static final String CLIENT_ENDPOINT_PSK = "devicePSK"; - protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 8331b99fff..4daac68f9a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -29,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index d7296fd47c..11b6f3f6e7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index e6c26a05d0..1f73a2a739 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -30,17 +30,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPrivateKeyFromCert.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCert.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, clientPublicKeyFromCert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverPublicKeyFromCert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 4cca303e8f..d3301bf660 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,6 @@ - From c981ff4c55016e4a8c153ed4121236c381dfa26c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sun, 9 Jan 2022 15:22:43 +0200 Subject: [PATCH 057/403] lwm2m tests add no trust --- .../controller/TbTestWebSocketClient.java | 2 +- .../lwm2m/AbstractLwM2MIntegrationTest.java | 16 +-- .../ota/AbstractOtaLwM2MIntegrationTest.java | 3 + .../ota/sql/OtaLwM2MIntegrationTest.java | 24 ++-- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 6 +- .../AbstractSecurityLwM2MIntegrationTest.java | 38 +++--- .../sql/NoSecLwM2MIntegrationTest.java | 4 +- .../security/sql/PskLwm2mIntegrationTest.java | 4 +- .../security/sql/RpkLwM2MIntegrationTest.java | 10 +- .../sql/X509_NoTrustLwM2MIntegrationTest.java | 21 +-- .../sql/X509_TrustLwM2MIntegrationTest.java | 8 +- application/src/test/resources/logback.xml | 1 + .../lwm2m/credentials/lwm2mclient.jks | Bin 17660 -> 20462 bytes .../lwm2m/credentials/lwm2mserver.jks | Bin 6432 -> 6448 bytes .../credentials/lwm2mtruststorechain.jks | Bin 2982 -> 2982 bytes ... => lwM2M_cfssl_chain_clients_for_test.sh} | 120 +++++++++++++++++- ...l.sh => lwm2m_cfssl_chain_all_for_test.sh} | 10 +- 17 files changed, 194 insertions(+), 73 deletions(-) rename application/src/test/resources/lwm2m/credentials/shell/{lwM2M_cfssl_chain_trusts_and_clients_for_test.sh => lwM2M_cfssl_chain_clients_for_test.sh} (63%) rename application/src/test/resources/lwm2m/credentials/shell/{lwm2m_cfssl_chain_for_test_All.sh => lwm2m_cfssl_chain_all_for_test.sh} (78%) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index ff6b004405..2bb68737ac 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -74,7 +74,7 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); + return waitForUpdate(TimeUnit.SECONDS.toMillis(8)); } public String waitForUpdate(long ms) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 13e0858199..0a95dfdbc9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -135,7 +135,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest protected LwM2MTestClient client; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; private String[] resources; - protected String endpoint; +// protected String endpoint; public AbstractLwM2MIntegrationTest() { this.defaultBootstrapCredentials = new LwM2MBootstrapClientCredentials(); @@ -197,8 +197,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest wsClient.waitForReply(); wsClient.registerWaitForUpdate(); - this.endpoint = endpoint; - createNewClient(security, coapConfig, false); +// this.endpoint = endpoint; + createNewClient(security, coapConfig, false, endpoint); String msg = wsClient.waitForUpdate(); log.info("msg5555: [{}]", msg); @@ -264,13 +264,13 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest this.resources = resources; } - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } +// public void setEndpoint(String endpoint) { +// this.endpoint = endpoint; +// } - public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc) throws Exception { + public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc, String endpoint) throws Exception { clientDestroy(); - client = new LwM2MTestClient(this.executor, this.endpoint); + client = new LwM2MTestClient(this.executor, endpoint); int clientPort = SocketUtils.findAvailableTcpPort(); client.init(security, coapConfig, clientPort, isRpc); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index e78d74bdcd..6cf35aeb94 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -30,6 +30,9 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { private final String[] resources = new String[]{"3.xml", "5.xml", "9.xml"}; + protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; + protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; + protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; public AbstractOtaLwM2MIntegrationTest() { setResources(this.resources); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 903750663a..95a0a774a2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,12 +138,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { - String endpoint = "WithoutFirmwareInfoDevice"; - setEndpoint(endpoint); +// String endpoint = "WithoutFirmwareInfoDevice"; +// setEndpoint(endpoint); createDeviceProfile(transportConfiguration); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); Thread.sleep(1000); @@ -165,12 +165,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5() throws Exception { - String endpoint = "Ota5_Device"; - setEndpoint(endpoint); +// String endpoint = "Ota5_Device"; +// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5); Thread.sleep(1000); @@ -204,12 +204,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { * */ @Test public void testSoftwareUpdateByObject9() throws Exception { - String endpoint = "Ota9_Device"; - setEndpoint(endpoint); +// String endpoint = "Ota9_Device"; +// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9); Thread.sleep(1000); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 95878b74d1..2310fe1659 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -73,6 +73,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectIdVer_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); + protected static String endpointRpcPref = "deviceEndpointRpc"; public AbstractRpcLwM2MIntegrationTest(){ setResources(resources); @@ -80,9 +81,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @Before public void beforeTest() throws Exception { - setEndpoint("deviceEndpointRpc" + endpointSequence.incrementAndGet()); + String endpoint = endpointRpcPref + endpointSequence.incrementAndGet(); +// setEndpoint(endpoint); init(); - createNewClient (SECURITY, COAP_CONFIG, true); + createNewClient (SECURITY, COAP_CONFIG, true, endpoint); expectedObjects = ConcurrentHashMap.newKeySet(); expectedObjectIdVers = ConcurrentHashMap.newKeySet(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index b0fadf1d23..c10eb46620 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -27,7 +27,6 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; @DaoSqlTest @@ -67,16 +66,22 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; - protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; - protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; + protected static final String CLIENT_ENDPOINT_NO_SEC = "LwNoSec00000000"; + protected static final String CLIENT_ENDPOINT_PSK = "LwPsk00000000"; + protected static final String CLIENT_ENDPOINT_RPK = "LwRpk00000000"; + protected static final String CLIENT_ENDPOINT_X509_TRUST = "LwX50900000000"; + protected static final String CLIENT_ENDPOINT_X509_TRUST_NO = "LwX509TrustNo"; protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; protected static final String CLIENT_STORE_PWD = "client_ks_password"; - - protected static final String CLIENT_CERT_ALIAS = "client_alias_00000000"; - - protected final X509Certificate clientX509Cert; // client certificate signed by intermediate, rootCA with a good CN ("host name") - protected final PrivateKey clientPrivateKeyFromCert; // client private key used for X509 and RPK - protected final PublicKey clientPublicKeyFromCert; // client public key used for RPK + protected static final String CLIENT_ALIAS_CERT_TRUST = "client_alias_00000000"; + protected static final String CLIENT_ALIAS_CERT_TRUST_NO = "client_alias_trust_no"; + + protected final X509Certificate clientX509CertTrust; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCertTrust; // client public key used for RPK + protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCertTrustNo; // client public key used for RPK private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; @@ -87,7 +92,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); - setEndpoint(CLIENT_ENDPOINT_NO_TRUST); +// setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { // Get keys PSK this.pskIdentity = "SOME_PSK_ID"; @@ -122,13 +127,14 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey(CLIENT_CERT_ALIAS, clientKeyStorePwd); - clientX509Cert = (X509Certificate) clientKeyStore.getCertificate(CLIENT_CERT_ALIAS); - clientPublicKeyFromCert = clientX509Cert.getPublicKey(); + clientPrivateKeyFromCertTrust = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST, clientKeyStorePwd); + clientX509CertTrust = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST); + clientPublicKeyFromCertTrust = clientX509CertTrust != null ? clientX509CertTrust.getPublicKey() : null; + + clientPrivateKeyFromCertTrustNo = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST_NO, clientKeyStorePwd); + clientX509CertTrustNo = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST_NO); + clientPublicKeyFromCertTrustNo = clientX509CertTrustNo != null ? clientX509CertTrustNo.getPublicKey() : null; -// clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); -// clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); -// clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 4daac68f9a..8331b99fff 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -29,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_TRUST); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 11b6f3f6e7..d7296fd47c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 1f73a2a739..0066014a9b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -33,13 +33,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCert.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, - clientPublicKeyFromCert.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), + clientPublicKeyFromCertTrust.getEncoded(), + clientPrivateKeyFromCertTrust.getEncoded(), serverPublicKeyFromCert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index 6ca430327d..b2ce6c470d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.security.sql; +import org.apache.commons.codec.binary.Base64; import org.eclipse.leshan.client.object.Security; import org.junit.Ignore; import org.junit.Test; @@ -29,18 +30,18 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { -// X509ClientCredential credentials = new X509ClientCredential(); -// credentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); -// credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); -// Security security = x509(SECURE_URI, -// SHORT_SERVER_ID, -// clientX509CertNotTrusted.getEncoded(), -// clientPrivateKeyNotTrustedFromCert.getEncoded(), -// serverX509Cert.getEncoded()); -// super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); + X509ClientCredential credentials = new X509ClientCredential(); + credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST_NO); +// rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); + credentials.setCert(SslUtil.getCertificateString(clientX509CertTrustNo)); + Security security = x509(SECURE_URI, + SHORT_SERVER_ID, + clientX509CertTrustNo.getEncoded(), + clientPrivateKeyFromCertTrustNo.getEncoded(), + serverX509Cert.getEncoded()); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST_NO); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 28a10fd278..a51ea98be6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -30,13 +30,13 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST); Security security = x509(SECURE_URI, SHORT_SERVER_ID, - clientX509Cert.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), + clientX509CertTrust.getEncoded(), + clientPrivateKeyFromCertTrust.getEncoded(), serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST); } } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index d3301bf660..175eda993c 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,6 +10,7 @@ + diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks b/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks index 490812c01ab483d3462a09cdd18bc405fa23abe5..ca8c8ed1d77f31bab0711a31eeb63ae952c933ef 100644 GIT binary patch literal 20462 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 z z8Ycse&LxPB6tj(~84FM=Ymk`uyhaOc1nPhjqtTVUuEPve(TnAH*8{gmfzk(e9|^nC zO&8dv0X@^?xRbtoX#HlQM3P%yl%fT-|Xshij+iQ&3{+cYb}op*0%%%FFVG`WXAnuzfs3vuZVyiwg+qhQ&#YfQmnOev3YR&Fs|`_w zvd6=6<22})n8JA-Pc`K5Xu|ITWkSh0{loqpH9TxHe5Q8#ZkA@M7}rRDd97vcr{(aK(_NO*AuoQXip%VO3*IND{R*i`{< z)R%+f)}YwwP^@Jsa4xv|@u4q5q%|6rsl5{(;mplDe5J}dYhn8@VG>ix_j5M!h~27f zp~PsH79uu#tr|%pj?%+;SmeA&?Y8eB7>_Z$TMI+WpRz@Sk*W}x4H_${CqBv#a zf>W*e-W}7yPptkfa~ov-1>zP|_UoGnmkUKZ6ujCTAd-1Z3pzcPP$`|&2h8Ag@9<+&Z*!iX)(*Q`Mjx{K\hylOan*;kqm$M}?1Cu_<6QnGg~ z@4c)8L2ihDiI_Dg$~_}ZmMSM00R;O!q{(3L%5{QgccZ$WDT;eQUC6u4{&3s}TUiEF zDrF2{xe`cVzW-F7uYXZn+l~UqERv{-?DEDn_AOJW4GR;dMS!L}$fTmxIWln{vk)y+ z7r3UsHv`E%lcDeG``N$e<0`V|w221HENbMn$@*t-x}hA-S^BSkB>p%MCZ%1`h4V>y zblRom4^)+FB)rd4I{2_W-w;z*8VM(Mi;U+eo`lwhf_`Q7A6R^O#x}fk8GQB(sH&A~ zEiKk$tEi`oO?J_Ql}l@bm@y@bmtPIMXoJd7QIb@dyE~*FQpu4&^+c;ATk~q=PIUgf z$od`Pc+{$dx1w5M3<~j0LuP;NZaqx7vPLbh!xl_mI_eam#moFkAu<)8z817p`OP8O zm0QGjeYZFPFc%_2Xz=}4Ct=!*$6wlJqp5;v`;oJepMYT4eWgx%S%>$Z(H*uA?R(^) z|2!Uc`-UoI&wnIeR9UrXBDT1(wi26onmeL_CWaT-o0}qd0-_MxN$ggSZc9T8`T(Mk zqCojr40so%!-p#sU`EO0U$$4Bj0Ci>Ju4wTlogY`ua?b1^DzQ|ujbn6$F2wQugwQ> z7I>#(yEWU<$BJZ)<17J;TknXDXPI)vv=l zbg+&Y{Hf?LwXa^^!R>Vo89r^bhvh|7^jTh#LYvCftw32^Lu&6iY}w3}#K@Z_Ah*Cu zQ9Ihg_~Z1xovZA$=IPp75ufYDyK4>9>E_K_@s}FwcL@!;EvPkhrGE`m8IY0d+fl%y zbLo?H&Y)S_Q2~hQRpq19JHu(NICAxh)^JUVBk>_*Uf=gf0A}ULJ&r1{bc8M(Y+Q ziG+RGIWZKV?moiL4(7|Fd^Hc{O@0nzy`)iQrHJVaIy^ z(h{Jd6>{%A>9KGAxH4D#*T}I$S2lT|+BAtH3&+ZYaPvKLxX&>~{T}FKQ%h;+yfbyaXK8QX^pJ91bu|6drLu%c$u zOxxhk<-wRgzn>1!#}xzy8z`QKnS@kLLgTk!FZyB(0dOn0;6*12*Lqe)E8Os9jPUN` zN~m1)@>U+j8nGmzSiz4CTJr*Vu2J9pxR7 z6Y)vlAH(~I5@1gq)armBGXBpjQg|qX?eTah=7lt7%IRu(<3tYv+^<09I#o_29s-Be z7SEx`*7B(JVX&h!g2_bsIA+rx#IQUw8suw?TD*O~hZ3evi?h;HXs#oy+&>7t1~y%_&;2@!R5S*NyJ7Wl_-`qXeo6rW zQ1?7+v$DdQY4Y|E{4^v6ro9-@M%{e|+>u&k6Ps`fbd-dTLw;KNbr+-DhH&YZz1kKUB(;J(# z1Y-&8uoP+tC-_^I$=l1T6m`G^&{Cq0VV1ZVtyoS5F0xQi75Pedf2H5nFnZzS$Ga7> zf=v^h^i_E3RenJ3U4-_w_9$6m&I)CfUiw8Kf53^J3`WqmZS=0~V3dx$`@}P84z?hc z;>=+wRk53=JtC9pvaMG@T@?)8YC)h09&BS%(}2)qpm@H0#NSR)iX9lV31|(iVqf8d z&|7y+GI34;l?XD%xxUHV(`cjlFrji$xvz-MV7_#onZgQ{gZ!=!AT&*6+0InW_Xq^I zLF#SbdO4{6jyq&+`65s%)o^6yJ-y&^aV1QWaT@if&zxgKKWqNRoguqrEGKqFSHr2g z9&+xva6Hia*BBO$fHtqUWO)%-x-}_1&Zx?Iby@rz^=h9moT7+*UKSE;xz3PG_atBf z&BzGIbkxf&N)58rI7^I{p1Fa1x)QO58KbR4r~;#$o2mOFH8#tz)))-Bv$Km7wQR`l zG5IT2&`8JBNj-}~ZOonS_QgbM;5%i;JeJpyC}q~rVx0-gnq&wIH;8Z+8Om}|DlPGE z7a$$#v+wA((KHqT6lq6yu)Pu^92i+V)mH zZ8n(1jcw3te^J3EtVn{+LrTlqzxU6}12bf)a7KAL^4ZMM@$k%hmuhlG2?p7P9x6yE zV9ylB5GGK2SdA`_cSwAg^wzI(nz@X#6`PJLBUDwg*Xeb2!0|CWV~IjRqY?MTOS)g` z&`-@DA)_?|Uk{Qmp$cHbK=bc9h%%Wqd~g?n0J(4dCdO85Fr>x4${Di^{2 zNn|GQIm!4&H-%AAI134^ee`Bf24z>jkM(kc3t){iu_q^8)d*`1;`PPlcD~xjznFrk zcN%s!77sjUJvGc2Ie!`EOB1X&xqne&j~oK&fe>3o-ajnWv|1zDbd6Iwm0{Gt1RebH zNeSdRxY*GCk*Lv98ES2mJHkb%&wuOV8Z;lth|TatK#Cth6}~(RTibZ5J$#;cVl;ky za4>xU?qrEfmTpl)c(I&xLk#PMd)uC5)+u&maNG@gnO?dYgx+STTHL8aS-Ll*5wW5) z#)9*J_=1?z*PLOCNE2wSwlBY$O|_O^lJcctZ?ysDHGtOs#l)694LSEHAUO3Eu0?n}HHOiy6jhL~aa~$YH16C$iB@OyYGs z&*1K`>)NvGYL+M^@kS1>qW=1+L&a<|k>^mw4QampfZSs?=4&@T(nMjFtk(oO_YtX2 za25VLcxr6r9_Y*(oV=D!7n z%y(Mxi&&Z)*+r_C`s5W_w#uR?n=hU%*vx`5TJki}#r_n6 ze8Ae}i*Hs(Q1D{$ciIW-m?sphWYu+Dg(!VZ?VVRg7Ol#xDK4|>9%SmYhlk%+CbDS# zZj1v?;i&+R`=@fz?>wfvjiz!PXr$t2jCb`?Pi}jkVam5kQfoaNV+Ea@L3Y;`nHq4s zm#1g>7zKcQ57pi&-AFUU3_}@m!|W$7Cod3ai#KMgyg3K+-Akxd_U%J^@M7SQP^oA9 zKJ=3v*Kn2Xv{^IqdMGu*6g=~kI;I{L1zHcw1Op%8`q_x(EB2&0gL;@V1*oJJO)u*MPJXELw$>e&U=!0rO>?MrK?rM>&p*-r?pg25)KgUoi}?0+DDiu z$%r_+`s!WT+*-UiJ`?hBMtQ-&uPJR}h*fG&~u#PG(PnbrQOn&P7GT zKzn;Uy-;$}+e3R`n>=tDz%U34O{+$nV`xDG&_}UdUUmw!8#8v6#STLfXT+tsJ}FNw zBq!32j-Jx?)>7d&O$o-cpgo8leHcMynggwZ20*t*O zo@dP7-J(z`Q)mUYo5%sQ1g_#LR9?=dO0me4+Ns+y3M?9uptc7B zP*Rj1D4=&Gbq2PkgM>NI1G%seJ`=XKQfUHCfMS^e`Bi1Y(090dL6DuOKWYo(mb#LF6vUrB z<)GAroE@S-3A6i-JTMCrqQ46*YF>BgfrY=D4|TV~?h`(bfo&h-caR4dw|%3KW@?}Z zc6#bB^6Sr68!+5R+s;l8%zdjEEIj7-!C5_^Uzmk8o9rI4Tn=Aw=q&^_l*bLt zGe7{})J&?Y)g^I>0N*gi`}uQjd15xx?MtqSEktxC1BX~OUZ;wVI1Lh;QcF!YNi|Rb zuR4$PyvZ=tt`jca@u_gmLL4V@WZabYnyP8Cjz1qIVKoZpEd!!MOu(U7KR^gW@v`o~ zX)k!CZAv*o{`Cv9ibcIpu}eax2rBGjO6$TH@=gNg@|1jlvDE_qPMwvjALX%<2{k-I z;S!31x^C;Pftn5`O$)UoM>_pH7X-x)7Zv2uxJ{+z3@a~DY0F(^YRWV5He^2X;`a@@ z_r9z0jYB#vdXM}(u9M;yJoiRfO~m{uZ2JT2<9ta35Mid^Q~8xTfDAkT?=-(46as{! zHH#5R`1gpIUYn)`PW3!d)jW>Z zkel1#j5Y7jy4;DNAnHOfHq4P9hI&{C_{Aj@eE?k^p5x!%X_O>i{SRJ`xJ|sh@NNf` zHC-TK*r1BkhKzQ;)wRRjO-qoKzJ*2hI4zimNETO255P6Ikxd9mzSR$`KL5RTFr!?` zA_D1BECr?*WE5?Z5%!X6Y~PAJT2w*7*&Upki~8w8@IThLvZbz1l!Q%$vqD+MP5t)i zw8HHcVVd!5Jk(Og>CT%JhUj2e=_jgqQFH)$V7w<;FAqrKf?uW8JbgQCh?gE_`fQ$f zh#%fix>(+*HPv4;GmO5J3f#-7qkws=Db_5>T zQxTEvhZA|LSyvdEAx}4a3X9Z?AS+?f3Xh%~+J$6@(CDT*UtN3t7JpHa`l3$5aBP9= zZgN)xJlhetpXh>6_DMTjayj5)2NWWYg6kE2%60cHuZ8V|uOOE|G*sW~jQ_qM%YiFG z0%?BTDNwBWz_?koE6IWq*Hg3aYqG*x2US}?l37*72!uy)FxdRc%ck)zYd?2LO|l%P zZIC>0pC|})s&RYGIvNH{qR9?{*A$wJdJJ0mM9I;Wo(G=t+yy~d_MkkiXtkF+xKT7D zO{*o$NEO=`H;rNtnw**pqF_EmUNHmi8=26x;fX7jHz8C425xG2BY+>>%Bh@y-q-D8>w-dVZS7*zOxibYS` zEl!UShaxF1xB3lF%)C@m)8y9?OP}dpu%>2CuoNpcjhGq6LqPXWnQu|2kJdGAazia- zKU|C0r>+aGDW@E=&CulGhb@LAb&awoBU`W3JvwC(0sgM$@BT9K^^5LfG@mr zEMVV%cFIi*4*p83hhD0|n#5F)%sXGTUV`PTL}h>M?)4ea>aOju+BuCm961wlkgpom zjE2FEnbmC|!pk4Jij8=~LoifSh_D8Yh<3C)2jLp-^CK;Ly6(!K9*|9{I`Ub#^FdKI zI_GTZ>G5C+$T#xkwC!Yyy6{PN*ImBp;K<8&QG9qOT0F_**Oz#`3nYy#TS1@wrS9f# zuqUtxzz*s#=SV*hGK~8XD6!xkvRc2Yfxb0mzjX6XSQsCEkVz1}?;D@8&Cza!b7nCj>pK#Hm zax>v?#bC+fzGs*Z9VghC_!v%8g>gSxz9W0E^24z^ZZF#ml|xpvJ1Fv%+a~1|sw{iL zND-yd9iCPDZrFEM+ms^q!%EdV8(8fkv*HCOBrh)>?@2F-CpER5dVp6wc)g#bE$L3; zy8F?D;krXs!ZVd0FkPUXj)MdXkC>;neT+)>cYh{g1IQZ~?Ub~~FSQp=2k|*%#PT8Y z6EZiEiC)vfj{^Rkg^M^`toFx)pW+RSq##cCd#rZvu=u^~T%YOG|E!eC==lpzH3l~m z<->zQ5KW4YX%+q_?tJ(On*ZF(0o7=I6#1iP0P-unotfZoP-4ve7mZ8Fg>o+S$me6< z^5x$9R$?)Ae&4l6HzbdAP}Fn+uQSFJ+N|q&2N#p=&)TP6$qMCdPQ&pldINmuIS`6895e3&;a(Ph$DE~gXjsO>HZqm8Lbic+&@3&9^~BmQ!AjYu$d*EvdzmJ zyGBS!Qp2-;s3OHCD1@>m24x4z$1KFGoEq1?u2>?_{Ae|Cbs>BRiT*i!#u$LAukj8? z6?+N!A39Cbe~7u=5;X~_Lzf_H$h?HStJ>2mU=k|`-1PEVz6Tm{V|$(Qu0Q%4vRdPE zHYSNP>LdARS_w&&woR)*ajvd19-tH(Js2D*QtI$@)=QZJ5b>DYQ<2Z`*ES>)#ZX}j z*il6em$c;AUTYj1;gTv_MX~`VuoSJb*Q<*vcOsr`Qrqah*2^r87u2LG7g=Fe67Fq~ z?jqik>A3UF#qCr$!4B2AiX#V~fiIsDm~x1?*&BI5vp7$fi@8{X@?M+udp zL@J^sH-7fmUIgZju)H0QZI>g<#fmg!t5h#0I9+kb8Cy~q-#!sQIGS7JO2O2xsfL{LNlei7H4K?YL~Qs{KHUV0h)&-0xGG9qv~l^z~!?9%A5!IM7D@6_^e z!9@XFOF@^Xl27X7z@ye^(C&s1`bs;X&VtIOgHRt7TB@g6imLG z)P~d#bG`1}Ztd^X{?!*#+~=hYL$NiNGf|LhacpZHS^F<&EF!+gVB zZaU*wf!iJg5KChVD$nTxs~q0k4<{{gm70D)K$jxd$kz$-yTju0;O{hyjEL z!!MIi1!cAi1ZLQ7KGbJWPlqv@PO_XFgeMVq+9L%QWaidpe0`5GrodQM9hcr9 z7(n!ePr9b($(rc0>kZm(b}mP*-1uA}XkYOiyu`5m>6ka?zPayG4tGuA$1qOx4r^X$_Ljvr zL-}pbp*mezfC%a^D_;*+-l-WG!-OiaT{v&gWpv{fGTW6RO8P%$l^DJzwONmOAYmGO zJtAz*YplpLkZlGESU7``jmdMbA$Fl#q}=!bRo{n8GR!@TkK3L2{j*S&o_3{Cz(Py{dVzjZ81GxIko zc_6J;MzwV@($MTIWT(mFTDL+zH`9s%WQSLH;Swlm==x^vQ8P@~Kg{cd-2_wU-SWl* z<(ve8w#&>7;fuIY6$>-7fiEdVWl?q)VHq^rhXqK4Ul|}SSy4Igpd`OCg+O56=bTdn zsBkfBhi^-F8QYeh*A}WU(1u(@v8Bf)>i|CjLm9?#z>|kYIOH2bqu_D%=LzAK5@M#x za{X@}RUbHvlKpC{5A)&c#`CTa{XL6#tUHY}xL+t_C+@shf@U4?K^;V_LDjuq<%5(H z3@x47<;;o*h3){%*RGtkK5GG@lfgwof?yW zYx^j5(-PKzhl#@katK|A{LrFBcovNjwKv8F;G`eBqUOHU6Hrd^4YcyF+eG=XQKgZ^ zWj<6^fBqhze@9mMPC&zscdIt-cXwFw z#kMzYG~v$+yGLJ2^IhWZ*+^6+)Arn(5+nj~)t} zv46>nF>KP}%lDoE@d+S5$~X7LQEk~wU} z8xRCVOWc7@N+oMi-l-%sTHP36`~W|OpL@cG z!|)E@?MD`Os(WOMt2sUz^@JlMkpQMGh+b*805@nT<~^S%Wr#ysdeAGo%}y-4UgCF2 zDc{u~-+&3YSOO^k*rLwIiy7Re0aZqjx`}Ak>yd_}YpS>LDDy4}b4!Uw>V+((7J;vk z#fo1yC6GV=MM4ASl4lC3uMIm~DAbRQ<+XKU<40doIGCRFVc z347rKduxiw3Ms7?5QmT=k|l8yI|{x!luib7&!g5HxJy8~{>(fgV2+Ndm>fMDB%nQf zN;vZXJT6cbyRrSMQ7tD@5i}DK!qGK+(fA{FhCCWn+=><_QW70H?ZZco6y2>-$eMPC zn?6d~eJts3X^yAY55Xx~H4m2o+n zmI$fg?&JIB!K23*XL!sz#nScB!Q#tuVjO`Vc^NW$S9>&*`)9Ezp1V|4?)6_GiA+t}ZYFxk> zGMsr^URa*Fg#rB?_VMBd{8HClk>J`8M*|Nv-l}mH9BW{9Z=_i2`cx^ft6b@X78O|b z$H7-n3YC{lDeB3<1BjV8PUQzCZ{sxXS23Y}^gfvvkgAg&&ewwu(98Th2Je-N>>0xI zgcD;v5*?|n+TCk3Y~_IPUUOD|cI8QJGTUe-&+PalN+7c> z>L^c0(W=eMbguz|EG}f7%wGpSfMQGRe3lB!d^aQinS=b68(kT!4Z5&1 zbys`%>rK;V{#Ul4dgh+zz=68>ScKF^iV2|Net~(G3U)!fdALq-4FJ8%I*LWby7eCg zPoEn@;uS%zEU;VnbQ)e3CxNHn6;hpaZj^oTu~o1Zb#5)jy00A)wSnDw84mbL^yh=^ zeG`%;1eOWM-|b*55-YXN2A!=Uf+VoEgvfSd?fT_2U0&effWaYiWnUB+m z%qn-GddA5Gmux$db{!0PgrT%#BOrZe=x~~kGKps*AP$wpUK)5@KmtS`KZ_1q`v%SI z$z*@Rf!x?5+!nZsxfY@TEVZ_|FUHD6Dc@JLc+M9Cj-m5ABajU-?!|o+hJ2eTfkqVJ zM`B*!S`-DoAtUoeXq-_7u>z(R9-RT#V+0!w`8pD!&>$_&#t@2cV9x{Ld@fwbmk*&M zAVlHLPl91F!?a|1I{P42ZA+3UIec7+hb=LA8`$GjY{$-wlq&Kl%rYWZ=@#OF`G7I1 zo`RV-2F$xNmov;?T{IvFc$J$4EbcovTjj5ZVq7+x1Ny_jUU{z>7$K@R2uhQP zkTK-D{}a3mL-gBU#CQJJ?s}c-YYOcf+AA!@j;;Y(qmi7|S&r6Q;lPSe$xuZJdd0ex zg1HCCuEb*vU~&l+F+pk7<*IGTbV@X>z0W~lTPU9iJv8(DOfzsjzfI%{a<$C8DQ3#r z!8!`wO{Qm%GAzuGhy8=Z$HKtMh0b8{axE$we4Ss8MDISN1)}FJ{?VDi%^aE6UQ=On zeK4Nb)`3x=tc3DhU7AZ5#N6u0Bei?zDcuTWflhKTgGkbVaAqFR)p)5ap^5B~6GxPN zhE%cG@8vaH!M`^TMU0W7q>_-o5|a|LwcGw_r4$1g|G8T7ALu<2M3atf$+^3WOM zs%!1j-40?C3V9UR$r+tywaO^;Ua;XgVm9o_R{r)uPH0V3|1gNr*d5_0GMo)}9VE{< zQNr#3psk?z(}f)z<4WAXWhbSJouDdcY9H^s&$8Y)KLN%-FcL2An&Qg1#qpU~Aps$k zSfb9z!*4U8;!A~eFq9mP9@Ng|{A9?;Ym+k5rHg0M7oODhht=1OJXf2z(#LhNCUD+$3!*S$bW3VY%O+?C9M+jp z3uwYSt2z9FRpPkDYdLpkci}suevG!f7`Y7X)G@Cw{-*bzvu-x7-{RQD)ruq=W z{jTNjy)jXN;d^H(4oJ|z;V!(A8;@O_{P(KKzUQdfI?&f0*68XY9GMVuJUn1Iz!`(l zHL{TlT*aHqvQ<&g0%|mit3Pc545k{a91O@C*atH{w6AmY9y|-RLNu6<*gbNF2BeBs zzCLp*8Ntnn(k}_lj8h9`>Cxk_w2G#DQxU!@Z`{?HRF#)9x~0b`_%FZdiYHmUQ+Huh zD+I*qgj{QNapz!tjYLi7BN#|>KJvZe*ke**=jeI+SHgq%ZiKDi?kAVNf~?vgZqN2;s(@_Y8;&{$ zFSC-tp`s-9dQ>ZEJYJBm{GqkMeyg28f=W$#Geqm`^OGeY3k1Q4ZlvGXhkDuejHNNA zC&UZq;J64ESnUt8R!EQCYi6Xgykf1QbYh=;F`P zfucvLv&|8IS8DR3h@_@U=m0)c+c_Bo=Rz^n@Uhv=v{295^D7IbO=&Rx02>)6i0)8p zgRvxP2#%@n?Khe7X3O;(Yf5Q9Qj01UcP`mssj~3yc=rGQBR6s3R%}RecVQpb4`_#V z$*bnZmX)N4k^+oM9K>6eMPq7AcHqdTN!;F1t(3$H{S~2_Zz1i6QafM++)$91;t-?g zQxBGS3lt*0B;!o3)n=7c^}s~-$PY}%YfOf$-}RMIhC+9D$d0_L=+LuZA($xbER@Y+ ziz=sqx*C(2Zb^8U_ZXEow}+7+KRSR@6B4cv8kmYp!L1Z-Bm4ukes(icu3hz263lcN z2NUK3n}3b+_dkK%-;okFRkg%*Nn$J48$BNplQKxu683(QYPO0LqnOeUJkRA@bTU(> z3(&`i_BbikI=*3i^@@-fI5^*O8sz zsNE|k@iqa7O#qnVgZ(}Ofg%X~y9igK`|+JT`x208c~%6V-Qw)oLdiU~<>NIq>*3au zKg5;70$K$x7q2}ZGHjRLd+88?Zc|$~qnm9U+3@AXq5=rnjrMNZ1*2g`24N7!UDd45z~**kcFX z$_mTTf`qZ|lnJ`(RI;f`-7q)%Yg@U&E7O0sgTyLu6jLeL3aX*gM`ryso=du;zM{b} zQ^`)sIYH?kkS*_1ii#4c#rG5L+-t% z*C$CnSqAkB#^eP01O%Uq-DlH(2!>KVop1L;_>6ya!a{(;&bK_iWQ_kBsESz8&++4LS5GK34w*E^TMvHx4WSlMM<>`+1Z5B{{oY0!3 z()I?ga>RB2H|*A)jOGZaI(qp0wLr`GQvTFzz7TCukd+-(%2spiR0EnupJvzle+wPj z5YTIA>D-1zSABH)oz=k`2}eU`Eu{TVtZsG+>4O89C|eb<{n||6f+30iJtkRUu1j&` z(oY6-=67(z)J0Z(u0=5xZCB4!Cd0s1zl;HP_J6w44fx~(4*UH+;<&Skk@Mn!-Cq*O zR&Jb;$Sq7&s);W1BH4YipUVb>D(>?Vh23-0=cfrO0l}}du6O{)epvEBZ>awTvMP;E z8!XWmRpamc#7iYU6|ZAKt(2dQ0jd(4Pm_=FVoZO4?DF=5JY>|}1S<1-zhqTHOYduo zfm$AD7b`+KPWkXP4TT7eC>dM{B)LDDMSZtvRcF{=03mh7tv&a}6ZLlr*Xpz}Kz-I0 zXt}P91y0f=Xih9DG;IdAcwO*w5Y#La$|95P`Ky(Gc%8ZL0lkZe|E!)!Ok`(BSn6~{ z_`KG!~ zq7GTrol(R*mvZqP%)~rdBil8!@$1wl*}r_2W7-HG~i!Y3#x!OaAs$OtOX9q2UwoMOLI+ zkKoeh^rsRp*Kj4}ue*cBS1|{ZSe2KKb>g6AhPXGimuCm+r45rh))g4T5()-zly~=m zocg(6AVR#~;eu{n!!%*(hfxkpl6dm@<_UcmADpr?#SzxySV+;Ag~Q)D#bNn(0At}c zUy-b%9oa^Wtef8*0E-6>B3Q);=g;up0DLlAz^hV50xr#S7~`ZV(=EKF5^Zk7N!sua z;@*m?;Em4+Ah|keG$oAL9UdFx2`V7q5fqrOI$sMIzO+%%%skWi>M{3xbdNSXdpPmH zc-*aBz@xwbZt?t(0KbF~@&oZ&sai!ZLyQej4F@KB!&SqQIA#a5 zuIYNCcNujy>6lcRqEf8xTcDnH^Llf8;3}PKz2fP6jejkJhAAItG^WVIqV!=-QheZN zU-=eV=^VtuZ@J2&Dab-OYXX|D(ABCT>zb9H)6v*Xkqcz2w>3Oy>gA158{O9P6R)uy zXz9Bb612*3Y;s1k#unsYTl)Y?t5_a|Ne&!Nacr+lO6}9@V4L91akpcq&hM+@a BIWPbK literal 17660 zcmbr_Q;aapx+vhWJ@bui+qP|+-`KWo+qP}nwr%VDIp<{W?5nl1cKV{a-s-BPp6*nq z-%im72)_XV0@DWw*MmV)hKPr}!UBQ=<^>2BfCLEV{1eWg4**yAA5GwbAOYaK|AaZ| z0|0Hnp#Og_7$`uXJpUhh|4(|;|GFL&mR^IN{lBX)^i&`yP`UBT(;ha*><Z#*DKPyl)yFi8CnaX=Gb5I{;01aQ{~^Lf*n;^*nhoTywq zGZ6o{KWuMA1xauf8CQ$NmRxK4wU)qN#p+}Y0z||rxMfcO?tD`%fhTAKHz-MkQM@ot z{ST*=Go@iZr&K0oc@0v^aTIi^$!CI2$HgewFy20D>DN# zD?JAz13fdxe<;phZw1$`R?_|=tj_&vt(@~IDKpk*Wye%IEA(ssxY#=8Nskfj2D*8E(53WAp0 zcQ0=_t0tjQ^$~9PaGG`u~kM&~X@{ZApwN+_f5fT0`$5>QBov}a>&r}>uotbGY{QrN zWr--T)G4_!rEubWxomU-z}OdXe@TC%{wSC}pnxMBsf<4%(czYes9NN+igaOL2QnEN z!A{?mmWEqjGvi79USqJ9wnmswP&An!Y)GqF#+&Z0&d$p}6-aEbPrlzj&k5=nt|8a` z#d?vd7z51O#8~i5=B|gAvZ(ssh%@~QaVBOK`v345H{gblldv8hyn2b;XNp5;p%F8K-j(U2HB&T z5#PDU=R0>42<~;F&{0E@iGM`WBfpt9`O6XHq7cLU^h=2?Q4z2t^OhL;Hz2d^!6|qe z^Jr!+4%jY1HspUJ&ipULnHgD`=>vEK{%f%Z2g?iKVf-gV3j_%8PeJzIEr9>(T{`)i zX>#=xf({&nw&UZV;gmcrPVw*D10v#Oc0#l(*~2e0f_lE89KQhWx-;_D3SVbCDSVJX za>GOf^OJ_n4UbmmWj=cXklPq+sls!q_#hg2oX921O)CKu*F(1AwNrS&)5H*w4vVYr zh{w|ljynorf~$~O^m{gV%whCmBi11mTOqJ$ekU@JIn5gRW6USw_=c%j*q4d$(#z-w zR$Wa!_aR)V5!<-iD@kNIc94ukA@@NB~9 zv=BMnn=5bKh3A#0eAfmrz^J7NA>aJ0ff`P`<5$hwL%X9^VZn3U^jL?><`X#f2 zzp!&}Ngu}Ovjle&tL{Om;V)`Qv@Q2R0c0#It$~MXnzPpW3ZqvLOD}|)2;Fld?B&5y z*Vt^GkV*AIF7u~Q_z}3$N`wjF>qB`q-)jmOS~tc{08<>Sn`o$s1Q={0;N4bA*O5@A zwo$pYw$6{26~S%?;d|qJFahyTu9ZE5q5R94YVctdLew4VfNta(gMZFa2kA>-}iuaCUzfWo%^sL$GG&xQc79Pq8t3DGJ(oRiNrw~ zyaYPDJh^RiI#Kc;&|ohNh99#;YY|6jI}=_EwSd+&=}%d?nS76e)pI>t52<^NtFsLGj8!t`5{1~+aw!%Y zs_&m$Wfoq{bQWI3Bc%)|tp3E$y95qg5*pa??!|=VLy?f-#S(0cLn9T_QsBn{)aJp5 z7w`qy0>tq3$IRiL01dHKBH!34=_M66JYWS#IW`t@h4zxV!K*#eX6e;?@?J^voyG>h zngMrJ%4b{PG_`&yuMBhJrL+%9X{-r|$Pw`}qntpr!NCdi>4A{4q)))O^aM^V7M;H; zg!+5Kab;r5#usI zGHB`nji`@evG`0PF*m&Fd&)==U; zs9UG%f(|WhNVrWd7`N#|&*(PEgLrlCSB`-Y0(?UQRaQ>4ocC3y$y{u9WX|M0usnfv z|H^>^eIq28j~Yd*&?U_)jHC$als0b|CJ=-B$#9dLOAJqhOt8` ziU7Et5dd;`t?{V3_C%&Uj ze3W?MyF(SI*%D+yG<8L~QSKF1jqFXLjZ;npHQ*s9^EEQ=BvvRV&>RH+Ru`?D>ihte z*~e)9kdc>H(M_J7}Ub+P(fUUaq1&j&L(Sr|p!_!&5k|vvdXxBE#fZzv8 z^KGaW?n1JM(`Jwfu79aXSWbi~gB$Xre9qxuo&FiI_Ct84$TEAtT45)U@zjPrl-`#b zD#=;vAM(}wCog37sENyMB%i91ea?Fqm`Vv0x>+~?Kb9EIC=E6u@HP@YpwBs&uwezF zbONQjM`H!5iI5DS4uK6&!rskUM;i)~wsY!go%Rs zc*+lsK1|-0H0X>}IuZ)9YqwA7S!=m&q_uyJFKe_KJ1PM^nxS8T_k3J@R71=CgeU?XE<{Ou=*5{6X{+PY_&zh&STzsgh~ zT}s4V?PJs@%RUfV`yKen2)4nk$$gZ)xQKSx-kwRDE_Bm`&Ysfgq>Pq4*=g`*hO(qM zA`)MOk8bH~XL)n99ANI!mZM6c*L>y=#S?1nknpt09Fn}qe|4>7?hyzYv4-jGc@YXT zsc-=6B}<-Nz_(LwL-oXE!+PhsPLUVXP~O*&$VT7ssS)^%yK0uh5{neT%3V4woOvJM z_xyzTaU-l}H{_SOW)<{Ys#tKfU!1{uY?43$M`C25Fw8XMldZ}rB&E!YeOON2bndEd z$@zcy?DP`>^%s4g1a}E-czPU+TA8$|_KQVA0mZ_V&9;Fu7=-n*|U_ zELkybz)erfIf0fq_YU%K3od_vYd55XBMQ0SfGR9+;k8|kR*l7SlE~&%b78};Q>DrFa_9V%Up&yPnL7lP3? zjaoJ?o)i-J?o(?r>4h`D5RX{ndQZ3t$T)DZ zM;+%XoKGSwKzF>ALW-j~VkEg(2@yKR0qp8I`{;eU}t+(DleenF>jB#n@fYi5qG>q7p`h1JY}v=Xzci z_?Kkd9qYlTxxE!`UA~D=6lt;PS;+>x;JOtD5M605+#HtEydR7(7h*C$Ac*jrZ|Qll zDpY)fNs-JYT~?IL393HAW&X}73H;ZaOmZEto>SRRb$x!KNSs&@FqaX(6}S?x8y<(2 z)a91Zmk6m*ef>p=a)9+!Y;Cwa5T|x#wQBcz_D0_5s}K?q3RuVLHg;&?1wzY~t%YhE zPBR#1Yye+KqASFu>UWR_Tgd_sT=Aq|4J$Q1JG`!m&UM6X?ECRi)s)<0=G)DfHuVSm zj@36rqJ-#i>XHfNGC!9z7L(T)<(6%9Gb>RzHSJQG3z;jQRFqQ@Ea!r}Kg;0ig^>=P zN8a=Nw7^cGz#jA)Uji?67m~>Pw#d0VQPh?uGfgg@c|(=@hyRRv7!OPwGt@3Sj7jRa zcm==|wK&UlfXFa^dQ+G>+9eKZe8^8gP@*I3$hYnOeM2AwxcR3}`OHeAP*j82B9^~6 z8a?GP5c{D9EUcWf=EAPiUTCvkZUIVhK4U2e%byrnPuh?tvBP2yBQ3UwjdqPor;V=i z2#yax?~eDd9%$0E^>K(qvBYo2e&S!XOkMqEvQ)@EpQfcqhS1BpGAsKb2i&uH?&7BL z+G6SVZmg@2jYuIgg!n0IhB7O|fwWM$7MDkKB`xESb{%x%3#b5ajtZm`-Z8M21zs+l zUyQ(paD;}DnRCa7Skg0qbN<=jb8xU9){C*UTyd6$WOF7$l$Zjg2eyk~w$4|@uv z(06oCI%4EK9QN|Q;ms*|v>IDdK30&Sc5~<85g8C(N*M#DwjMs&BKhmY?O=p!X(8;D z%-|cTalBFdOh7JgV|xA!YE(D^K*MU=hH0PzC%>?&_TNgLD}t*!M3#|fnH!5t;YwpT z&n~I-J0FJw`wHCIb)~3&-n}#n0g#pITXE$`6(WA|ogAojW$z?F{uc6G$XSFw!*zDk z2vHLE$f2a)SuRX5Cf~q1is#b!IG+}_lq-K)n)ZNBWy&;p^#<+(}v$@Cd zu8BA;!(3S*%AV4GZD^$RN&E7{1Ot;rrl1#v#tBb*gyJ%&kPE5;$i)&45yl*J{{#Ta z78eF?f)fz#?z>tFt^|30DAky$8JcyJ?F_)--;?1Q$Xi*~KISgW4@-F*hG#|Wb%Nc! z-U`eFyzW`&P*#uJ(BL`PNl(Jjh+mM5s@O-}hg}$POCRzbc-JQ2r-&rQUf=1EZ|}{$ zu?d*g0*GAi=a%bPWkepdNCq12@`ES`lB(){LieKYC_EaPcGfx-9o?!Su4=W@P~l1Z zA=ZFy+3@y-beSO4QpEUzrG>S?F`teipAdkA_&&%u_SlvojDUcw7c(NKcma2TjEe z6xW6*D?v|2(v^i0RC+#io6|*I$r-T}$^C;XMFR*L*)sI&mS1DPzwy4ALu*5efUDDZ z17(a7_ziPcH(Rj(8MBnEf3I#erN{JJxue}dc<3np;o#wlf#WA`vgCpIIz1t(l=vy?m8k59+sn*7 zZVDbUitt)aAsgpU2x#BHQr_6-fF&3fwWo~yTf+(naegx5bOGe?cPJFVKa;R#0vsG$ zjA#O0k2fOqm#8&nd!Y`Eai50paVm0d+#P(CJr739-)9L^37SqifMm#jcN>Cx)FH zJzEJZRpS^@rj0Jm6QeW83?yVB@5NxiNB2y&E)WZ$QK2C zIwr`2H3>zb*KGk7DveFe3r}KxL5I7{3`|AJu;l%EsdUq*>!G25ZI!}I^|1u;SkVpXL50Yt&*vD zFIgEzY#n7;=AR9oUYB6-74d0s8apJPipB#*HK5@BsLMGkW<$PmX#_rc&_=z+i(o>N zs74=kaRom1=N8)m<{?qD95q5G&7~LUQ&M8fT1FzmXk|VUS5<)L3sC^OpJ#7z!3hTC zI+7%^3MWHux)LuLPe5E3N?Ig*LSUP+D|oP8!O5y~9EL*wqQt*mX)McCD+hM)%mSY) z=;2eK@Q>BYN_9(hSHenVjKvu;fXYlXZ;YtSW)WFZt_9LF)e~D$_nn%LK~!| z0Up}nI{wCLB)q6#QL!_G^jNu#TAP%7Db}~qUD*%^pX8;ImM^ARM0l-XFlM+>08o`2 zL+4PO@tlzGu0m>hTra8f^kTK3dkjA_dXx=RsdDKT*(AJ}(xr~T=kQYe3Pe;@#06nN z?wFOZh#Z_CZf4$5;&uJWJt%JLYD>TFGLEvwL5DD!av>oadELw)q(Xpj3Aoq^kU91H{`_g1Xh;c!a&_( zKzb{*!qwC!dI9j$gvTM#E49CEJRx}lYoV`Ir+Ny>P!{CDo8O@@Vj`^*p#j338|DJM zIL3saO71*c2^bk==|B}M2b3ws0}5WAm703enN%rZxqlj8KL3%vD5%3A;0uImw z-Vh0K2wM?$KE$FyxRQD9!>BCa^xFRT1FjqA{dVe*k1L{BQ|ihoiwuZOdHa!IMgLW{ z`;vk-SP|^Q{TB42^i`O%rMFh`as9ioTFX$^}y&?89OZ_aI zsjMA2AH$*0w-EjGtDjfrJ+>u!!9c(@OL|*aOthu@1TC`aWOojHhJ!a3&TZI9>rz$p z_$n3cbIh9|xZ{CccgSkd*FQ17nu(!&uxz(qIug-D!cm@4zwU@Tpv2P!)TJI@U$UJpGikGW@iVBoNO?Up zZ)58S2+{MBak|N82zL9kp3A&q<#u|>mgr`LN~Lp>r)$%Bj?O%rmZs@oQ`|wQG)v2Y ztjDmD)GfPMt%-T$_+e5$g<4}!UQzW9r8U~L_Dv&5eBhdG6*N0K|)2` z?*Z-U)aw~#XbZ zDvT+6YrO!5elN`5S}F>tKBv4o&aWE?&XkcP@$h^XXjY-3JXO|2tRHNfAndZX9UvsA z3}|MT(h)ZIlg5!y#KQI_Rf1(Dm5@^#m)K2Y^4a)g_#N+aaRRQ-Be z#3A6SW&C98AF=s$q!8I%!+6A1{O=)B&%1#oSib_ETy7fB{yg3zRS--C380Uc0MD?S&mTWm zTsEj-u+xZD+3x2b84r-HzEf_+H4-*RWr(E#+Xp?I9c8|SiJ)gm6r4vCm_t@(yemTUJTF3wRGi3FM#hK>+Rx{u`F|v@P3D-?-gw85+ z;8}a8q4j9!m2bD!WWq>AmnqUn+SPL=?m%`z;T%vJyBefv?Ve6g+pUu2)8$ChwWbUPnQerRlmVOw$sy%J$o3flCC4 z`_eLkJ23Far+;uf^sj&o^|ymaDiUG1osPV5pda{gl2x0j@C|}ChQO1az%Z|DLW(ZRI z7#K76QM9;Kq)y;4A%8pFjBC#Mu7tX9Jff}LCti`cs$>@_g^R^uo-6$(Q5VijdMKDE z!79q-%Pr|S(XmznEH#Qbkq^Lpy_a15*t>*uckt(sRrU;aX`-SdW-Wtru}m9DB5kCI z`^qWW?-apfVX!8XIpSdLdY7*90PDA)&%gT($(D$jF2kS4dobmo5c&m3ip%^!f4@;B zd=A_q1EOi#yd(3R+-;X?agy|g(rFKv^w`H&SaOFqj6?4#bTE_ zA5vvcsm65R$kD5=!D=ZwVPdM6t5Lr>82mhG*(T$$Qg;G_k&FjM>t6kC#9eH!Wse|# z$bW$`F|!{}qvJKlfq!=AtK-!8Ffh&!|4cTD;|OKx36wX7;kZ7z8rSg@X^d|dMydHc z+Yv_~Lj*q;E1R74&g!F=;*#CmSa&*Iur{&ck=rYf);RXBW;w96$z2u7Z*Gl53&@Ey zv%c5iL4mi=pl9|sI#UD?Eg#KIJDenwYcwcS7kz6$adaL=IK5+KwIaIdVXZ;>&_QE+ z-Zr>sBScUUUbZn_Kucwad}>q6t!wz`N^ruUYp4^c^Qn(Vwb@mV#Sm78!&}XFRhz$> zLdFQm`mhkH8<}Ujx|a5bQ)IIYZSCYpkolB_o8csC`^3{~UTUOPPu^L2@WEQGlgCuq z=VL3`DM151Oo5eCvK4sB2+O zw5yrkq>(ioXC75%s-H##LB{Aj1(ve&0s2%PMaC-0rIadKFM4a7RFuu062ere;;e2v z(!EQiWkM8r9mc+KbJGHZ+vw4l>{(d&sKS(Ht-EF<3;5Pn;nZ}QT+if2;9uTE3qdYZ zS6@_iEOF^*6+%ZRc6hx=f02WXc3L2uh?9XnfqPQo%sR9RJNKf2Hm$02IGDa8cR@04 zNs(vN@2ucwaY=2GjgFsv@93g}cOPS23N5Ukrv2iG9@GX+8f#T&xv>_ zZsw{`TPk|=Pm;0b&eZOUFD}Q^@A?Vm3w`*~G?GRMR)w+9qTd##FPM64)M2SCC;e(pO2_-ZL zWX^joes!2a>ifqwq|pFH0Jf{;5o&xO3kZv<1z?Nnpp!3{xC~x?agRQ}#;-Lv4=A#;*g&F& zD8@VQF0)%{69ahW^tK=ogR|?VMH0G~o9rRVJdeCt`^_53a?>LzW_p@cFuB*m?9}kC zTTZ4Ydjv5cg~_Y}BVDGFF{P27EDLIFH`j_e!LPaezI7>TW|e&R!8PS1Bi$XqqXQKU ze_i@zXNtW~B}72T+luay|4Xk#roBJ!mC9L0p@g}S%fQ<@p;_%JGo-aUCy*vgDo4H_ z?&gn3vUon~$|h7NgyZ)quU#EXg{&jqLTir- zW9F0&C{XyZx9or@u&J!N7-1ji)%1Yyel0f;B{B8+}zE4~fS`wn^!ZaF`3j zXJnXsMs!iJmRps|T+M>@Rlo$)@m`Vs;XVXcjVbZntLpUGaKJ%PX!uEEb`uk007sFq5znG_Z3W|NVJ1q`> zi*u981sQ@bHKGK~>~mH#wMq!kmg?nWo-Pk;YBonlW*CO&#&rg^^e&&qO0T_PBOcu~ zx{&wd>{CO@^!6AjH=LMH7|kn3Lw-U)M0iQ`A>%)Lw-82D3m-V=!lKc@9_hmUX(r zMVkiS^V0PaE;Y9Aa?mpDy^cEst)+a$#UNXEmBrWai%)YSRzjV+vhMT&t;Z~tcz(mf z!nw4^o=n*-g3$N@SkUL_Op}^LabJLPhN}za)JFplJW7T4Gio1OBrg><&S7-7&z@*> zz9g|epB;yP4=%g*qkzzal3(pkLZaTDs(AmHsXBY^?W!i&PolGMJBNt3saZWzZf}CQ zh7)tX?pnkmmksy&)ggKIdH1}U6HEPx$Up!G|K8jih$*eVxZ3ks7Sr0}A;_eZdZ=h> z#A>PVusTPvRsMztVq}i};$N*kQ`YTJfZ(x@$haz2knw4=**i1K~BhNZsKXi#3<;nOKypyitO|o{uhM0UUv<&WY zTW0O5^TCNZnzLx1^bk z8{(r8RpCGTuMG8n)JKij^;ZwuC*K9px7cDzil9OKP#Hv?$&bg?+rqE&K|Dej+L;2! zbX6i1>}brDZ2Ft&d;5Jl|0a{fK_KMkX?lcmo`WFLFO$aL3j0vM!04pZS~u5HGhDCx zFInR{tkQU}(E_%9&0U5epqNl89EPMW07smeBsX^8+@AqXzfY49Tmugm4K7t29D=`V zJk)9ca(&m~8WFM}`L1FPgod&0lfRWh%x%R>rF4D(Q0GjIfolCNG_XPDf!?qd$bV%~ zsmdXO%mwpLQNE<4sJ>$pxrMWP5!|SU{qg}gWeVD77^^b4B-iHM1miE)F^tJeQ{a78 zx~iw0QtLe0#7aH20exE#_LGDzCp(po*-OQ#s1xbnhIdp5a5QyzX_rRa+M+K7_An{< z_M;ixXB~p?z1JNuD>JI5NE8X~*aCs$Y;gj};9(nRy}p|FP*djUc(%DTa^(%R%S3!429krhqquVX=Rrju%kmLE!VnpM=_TjftX+&@rD?eAu8 zWG{c4kNaysXf<-e3sM`K8p9dK?LY~b-eYe)>C2bmTELegq4#Z;Yz;-cS&Kt@o^e}g z7o$fgJUXGho|S>0e5sJL_FLBGS&kacR@}eiB^Cw5D-I8|aesO9#wUuE zRA4r9f89iK(7zd#?6k|1M;#mQ40)Ujl|oGduMgwSbzH_s9i4?Fg1!cx zG~@0N=RD?<33jh@?_>%#%22sd+HzGc6C41Lgl%U)vmwoEVgUT6*G;89K2SS#k?Vr* ztD;q+)ddb;9eoh4C^LRlR_m7U>tkgSWGO{Uq<4N(vR+p`J}7dBc?x^B3Y3TQVN?d`!|Y6BXly0HZWekw%ho=KRU{aLX?46F1r(}- zU6>-g(j{|rD*<_#1WNRd-PIi+LKLepK#v36DlI(*!4qrtSn?Wz5=$ILbX6N0tUTUP ze>u1zh_Yq<9D7H?7jM_a+9LE^M$5BZE7N+dl53(<*!99kSH(t^_0XsPz`@32J6Fh> z4%;sCD6|;>JlUf~C0+A_HfUT>66a2u0aq+1Pq}G&W~x?1SvmqpE|mV$8i&N&bb1`5 zT~l7#_Qs^R)`=}IeP=J*n6m8&Bt%ESwvZ41?Ca2d5SFH6d$1sn<%T|rx+(^Ya)C+4 z#`xf*lcE>7x_;2RawXP7rIo5{FH(L8;KTHLR8DR6C$ygf^nfkxz6j7Dorj@Pk;-<6 z0`#T01K(YAl`O?PW#*+0j&;f_d}F1Sr(~os12mJ=5QIHbn;nTb`4qpT8{* zH?rgYG!c}n&UlJ&Ysvwa{FAsP6%Qp}bCxhjdVi6#-yVnHgQ9w20)LbRJKtrC^F(F(eK;CYl(YQ;w<4%b}Qwt z&1&ICyDj~R&*w8|XuJe;md6b7O2+x<#rJ33$=b*1+ubYqFbf%8%eKq7?P7TDVg1<7 zzBDPoUIrV=gh(iIPys2dOOr-4UEFsL9d!mcv~#iSWdF_fET~xoNbV=6EFvX#kH&#- zcHar^v?~b+t2hA-`k78_o`13&YV5b7EFY??$O&q-!aVE9;(!QSpytpst|h>FlLL!R zo%cr*MxcM1Pz8Dbm06a~_;W2&s=2%A9>j)s+~cDZM|qG_@A312s>HY%#dL6U6lxW>aFho2jPkX0C}g=20fnzJcX!-XbN?7!+ob zi7Y!JgIy&aQ(VJ|$%~^M+IE(ycB=rw7^GsoT&l}}yfJ>yVQopCuH)*ksol0z5w&T| z-WBR0O{$C&Oe9Hcx=$-$QD>!NYqnj~w6lpP#o3{P%+?cj+m1$qvz2IXXQa?=&VD z2EgXLox_gHjkSZNu%zk?{j&mfYf{L1+B!w@fNn+GwN5L}puBoRh~Y8urw|vJqA|(t z;~*$6Gd6N%ciw96NEFHpK8`-n9}*)koxOv8Mkkza4@D^F-PBqJ~nMirzhzZWB z@X!VOS*ZmstU889>N6V`a*x_SNsZR=9aI+3Oy#Oj%hkg z<8s)MJ4lH54NLHBW?Vg$&Ht$nIoInr)DCP#@#VzYqzH9kPKAIwCeM;&mx*sD3{U({ zc%}#)AB*tC=~_Pau+LJkX`VX5JQC#m!4$OZsiDB_?g<#5Ibo8J5}@{@lB?1hQo`Up zL}p~Ml9}#HZ83jD$z)s`Es$bb>^}3MXn%zi+>aq-&u5CO;kiaAODI%L%8%0%q(jwK zHD*h?!Mz7T=JT!t-HL|7xg5j8#@lbp8n!M`7+x1u@2sZ2En@*`_|mJ4S!+rp07l7& zilU=~fCF$AD6iwy1)XtOmZIu~nz~*p>~9>>Txkdf$He==o0DeQA9AbYE@Ja7h1uWQ z{F9q($@-A4b#W2t)Hxwx_jFnL1Bu7((_UUELFqQaz9n`b&-~`-O{9+7a=1r8?v&6$ z5*pyU`jOvx7^-QN!oV#>v~79<3HdjD&lfeLO-AyQ8ws{l_JAg@2s^xnmUE1~e|9~}r3K7!Om1%)5GWP zAk@Gu2C^9{3UnADm8#Wid_-&;_fxeKg#1?o2uVs$In{E;3oiYuikiSY*B zF+tGP&QcRI&L1 z<*}Izyx|UNJvb+5>$N>eGL3Npd9q|Hq-(@q=$brD)7SJl=FB6f5~=rJ9p5->plggc z(?n-gLao{5zN)@Bb7BOC{$#E@m>4tzXa#q% z`Ze3y(>n*3K3^_+2R!PCMD7!l%s9>$Rl~r;quc1>Ho1{e1fj+X`}2orYKWK|xU4C0 z%oAC#r|lzhWxj#hfTcM3gEuh1lNN)^8jZ23IF}T|!qn$KvHLYU>F^6K=M8PeATe`) zQE5R@Y_$3nu$Q4S2*icOxqdE>vxYSe&o)hM>*hFVRS*0(Zw`9Uck0xp}S-F^3zt6cs&m_{7(#Zfg(A zH%23rYIWp8n4K|ahsMAi8x}E3I+|HyE2tgtPl zR_#yB7ix<#w||cx!o?O>d9Mft#gzGOr1%Q)HO+x+Nma$*(!3@1?JjtJSEqDU&!mr9 z>rmj#SmmGWx>BWwjSkpyNy^^5Tn(m-1KNbm9@eKttA_H$dIco-DHXhB=Bzti3}wpG zYELUFFQXr-zO;TzSXHxlRJP1pKx5lknV2n-#UXTgT&|_rIVT5=m8P*wFs?YJAPOPs z;aYtf(m{S|Z{`4-daU!ezP%u>*S--^v*z$s!$b?{n)jSO`=*k`DxId z)*HNj|KzY%^3428# z427|pa1tE*6hg5VZ-a^0`(Ty);SYPj6f2xAPd|S859%3PKwpZ7O*7-tkowIL1u*Mr z0xAO)Lt)AS&?!S}Wu}a0bsIKsz#isbZ;Q52v;AaDkXj{7*>4hM~egzn37M$to7yT!61&@jH5Wf<^#ev@g~i zHf(rRfMH8lRnIlfu6mPh(^A9e_Dzf%OXpNO(yXqqjN_d=z}Lr2uqfZRQnWN zRjlhj1n+ZTI;LkM)n`asrYtt~d#JY9We4fO`#>p6$g%!v;jn*dpFv1joe4vWRqXJB z5t4KuZQd!>SHijRs?;Pxq4yI$!bXs&Q(1196bHdE3x zhzWR$GWIf5_pbI%o$WzfqKk7F7)+Ey17du=@qME;b@c*8kz3tDWM_DH_)Q_RG z3@mDSEVqbB#1zfADr+~n;$^T%o~F_p^70cKBZdI8H|R_u8~CfwFnv#U-~CA6?EStw zN&O{7jSg5~b~>JmhFtL)fODFaR0n)3S*S4LJ%O4i($`rmj`|?Aslt40;B6EVx@3Vb z8E)h?a%15Ml(3VVi*qt1Y))g=P&>NB)Jv%hgUJOliXj`OzmU$Ug=>RwN!JwuS*tvh z-$FgZO>jJhNH}U3Arim>pTfFzEsAZG80BAa0^ruGSVp1r8x&%O>HVxgR;CjVtN6tmTn@qWeb#(r6m%^5%QaQ;JS)HfPvqb%z+p<7q@I;F5Na}XH1hv%+B*EC6f;_zkegU z%RJ9}{=K(WUk$*MbmneEaDINzkoO>6)EdY;FoHE?M8K1qbYH@sb)r7}2WJGikL_CRJUJfCdL4XxKG7@Tv#KofZuF94X#lk6+p`pK@fm&45j`7mneZg7BG?|Tb( zl?&3$q;CyE&yg1~-5fB{eJo$rRRo@$--bsR@VmTL*;B>Sd9G2d=W&5DNhzcFH&^p^ zLj@2fn|+!rscw}gh&e#82k%xF#Xp-T21k(qVIK z_4yiqj&}bAP5*ABAJ#{x4Z3Cr*tw^sprb2on|n`w{d=zi294qRE|=464RL^*abuUW z0;}|@N@drRKUFJ?k|UcDyvE8cjNB7JhOoXcb#x}dFjG$nwCJIjUNm&9(yBliLHF#O zo=(7(kTuhdO=`?Fl{vZ+)wCA^Gj@;`zwkuLGHe7pBR!0l5kpF#D>Kgy{95^c0&)S3 z{^op%0$VOPTm#xW!zx1tDTa+&hF*AAk5~Pn`s)Dug!jsm65`juZsLp&nc2nE%ojR*NCGn@#=YHsCUvCsh#dtbR* zDe)XO$g}_rLF>F2-+vzHs*oKPFZ4}6bd~9=O;aISt9do6_{HgulDR@mRo@fAZFMVS z4_BW>6bir7*2O11-%<_tod87})c_A33hATk-hHuq?1uIUN*I>%>EASCtC|fC3_G(2 zF@4t^dDuJMF~}AAX??BVQfXrVMVK}kl2lS~S=6Z8&nxpROzy$rG~?EJRqPd9Co)E# zcO<)EDobjzv4(?0SD*pCYei?MOu3ZRb;@JbunlnHnh>am#=Nt7j~&psXi6nFdfvwXd}b1Qe^47Q;6J?Lgy*av-MkenUa9 KeSQK10fwM$Q%GO{ diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks b/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks index a1923e942f923b8d84a989055ec50c2441f79259..d16967343fe4d56689c12353dc34808625155eec 100644 GIT binary patch delta 6129 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 DGMenJ delta 6113 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 diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks b/application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks index 2e46c718898093e907b646f2aa3051d7309d117c..b97f3629cda96a9c428c2556c4728310d0fbf431 100644 GIT binary patch delta 2678 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 delta 2678 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 /dev/null <<-CONFIG CONFIG echo "====================================================" -echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo -e "Generate the root of certificates: \n-${CA_ROOT_CERT_KEY}-key.pem (certificate key)\n-${CA_ROOT_CERT_KEY}.pem (certificate)\n-${CA_ROOT_CERT_KEY}.csr (sign request)" echo "====================================================" cfssl genkey \ -initca \ @@ -286,14 +294,114 @@ keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT done +#keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 + +echo "====================================================" +echo -e "Generate the root no trust in ${TRUST_NO_PATH} of certificates: \n-${CA_ROOT_CERT_KEY}-key.pem (certificate key)\n-${CA_ROOT_CERT_KEY}.pem (certificate)\n-${CA_ROOT_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_NO_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA NO TRUST", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG + +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_NO_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the intermediates of our no trust in ${TRUST_NO_PATH} certificate: \n-${CA_INTERMEDIATE_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_LIST_CERT_FOR_CAT="" +CA_INTERMEDIATE_NUMBER=0 +while [[ ${CA_INTERMEDIATE_NUMBER} -lt ${CA_INTERMEDIATE_FINISH} ]]; +do + CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) + CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${TRUST_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY} + { + "CN": "${CA_INTERMEDIATE_CERT_KEY}_TRUST_NO", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + } +CONFIG + #openssl x509 -in ${CA_INTERMEDIATE_CERT_KEY}.pem -text -noout + CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +done + +echo "====================================================" +echo -e "Generate and Signed the client no trust of our certificate: \n-${CLIENT_CERT_TRUST_NO_KEY}-key.pem (certificate key)\n-${CLIENT_CERT_TRUST_NO_KEY}.pem (certificate)\n-${CLIENT_CERT_TRUST_NO_KEY}.csr (sign request)" +echo "====================================================" + + CLIENT_CERT_ALIAS=$(client_alias_name) + CLIENT_NUMBER=$((${CLIENT_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${TRUST_PATH}/ca-config.json \ + -profile client \ + -hostname "${CLIENT_HOST_NAME}" \ + - \ + <<-CONFIG | cfssljson -bare ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY} +{ + "CN": "${CLIENT_CERT_TRUST_NO_KEY}" +} +CONFIG + +echo "====================================================" +echo -e "Add the client certificate no trust (${CLIENT_CERT_TRUST_NO_KEY}.pem) to keystore: ${CLIENT_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}_chain.pem +openssl pkcs12 -export -in ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}_chain.pem -inkey ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}-key.pem -out ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.p12 -name ${CLIENT_CERT_ALIAS_TRUST_NO} -CAfile ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_NO_ALIAS} -passin pass:${CLIENT_STORE_PWD} -passout pass:${CLIENT_STORE_PWD} +keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT_STORE_PWD} -destkeystore ./${CLIENT_PATH}/${CLIENT_JKS_FOR_TEST}.jks -srckeystore ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CLIENT_STORE_PWD} -alias ${CLIENT_CERT_ALIAS_TRUST_NO} + + + keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 -rm ./${TRUST_PATH}/*.p12 -rm ./${TRUST_PATH}/*.csr -rm ./${TRUST_PATH}/*.json -rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* -rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* +rm ./${TRUST_PATH}/*.p12 2> /dev/null +rm ./${TRUST_PATH}/*.csr 2> /dev/null +rm ./${TRUST_PATH}/*.json 2> /dev/null +rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* 2> /dev/null +rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* 2> /dev/null + +rm -rf ${TRUST_NO_PATH} 2> /dev/null rm ./${CLIENT_PATH}/*.p12 2> /dev/null rm ./${CLIENT_PATH}/*.csr 2> /dev/null diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh similarity index 78% rename from application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh rename to application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh index b3b114cb28..c869366ac2 100755 --- a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh @@ -27,11 +27,11 @@ Help() } if [ "$1" == "-h" ] ; then - echo -e "Usage 2: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" - echo -e "Usage 1: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 1: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 2: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" echo -e "Usage 3: ./`basename $0` true false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are generated\"" echo -e "Usage 4: ./`basename $0` true false false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are not generated\"" - echo -e "Usage 4: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" + echo -e "Usage 5: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" echo "This Help File: ./`basename $0` -h" exit 0 fi @@ -53,13 +53,13 @@ if [ "$IS_IHFO" = false ] ; then ./lwm2m_cfssl_chain_server_for_test.sh > /dev/null 2>&1 & fi if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then - ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & + ./lwM2M_cfssl_chain_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & fi else if [ "$IS_SERVER_CREATED_KEY" = true ] ; then ./lwm2m_cfssl_chain_server_for_test.sh fi if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then - ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} + ./lwM2M_cfssl_chain_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} fi fi \ No newline at end of file From be23dd2f7cfa43c1de39730d7a371208da06d454 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sun, 9 Jan 2022 20:04:42 +0200 Subject: [PATCH 058/403] lwm2m tests add no trust and clear comments --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 8 -- .../transport/lwm2m/Lwm2mTestHelper.java | 13 ++- .../ota/sql/OtaLwM2MIntegrationTest.java | 4 - .../rpc/AbstractRpcLwM2MIntegrationTest.java | 9 +- .../AbstractSecurityLwM2MIntegrationTest.java | 89 ++----------------- .../sql/NoSecLwM2MIntegrationTest.java | 1 - .../security/sql/PskLwm2mIntegrationTest.java | 9 +- .../security/sql/RpkLwM2MIntegrationTest.java | 1 - .../sql/X509_NoTrustLwM2MIntegrationTest.java | 4 - application/src/test/resources/logback.xml | 1 - 10 files changed, 18 insertions(+), 121 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 0a95dfdbc9..09495402f3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -67,7 +67,6 @@ import java.util.concurrent.ScheduledExecutorService; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@Slf4j @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { @@ -135,7 +134,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest protected LwM2MTestClient client; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; private String[] resources; -// protected String endpoint; public AbstractLwM2MIntegrationTest() { this.defaultBootstrapCredentials = new LwM2MBootstrapClientCredentials(); @@ -197,11 +195,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest wsClient.waitForReply(); wsClient.registerWaitForUpdate(); -// this.endpoint = endpoint; createNewClient(security, coapConfig, false, endpoint); String msg = wsClient.waitForUpdate(); - log.info("msg5555: [{}]", msg); EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); @@ -264,10 +260,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest this.resources = resources; } -// public void setEndpoint(String endpoint) { -// this.endpoint = endpoint; -// } - public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc, String endpoint) throws Exception { clientDestroy(); client = new LwM2MTestClient(this.executor, endpoint); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 0ea700d46c..8dd44d25ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -25,18 +25,16 @@ public class Lwm2mTestHelper { // Server public static final int SECURE_PORT = 5686; public static final int SECURE_PORT_BS = 5688; - public static final String HOST = "localhost"; - public static final String HOST_BS = "localhost"; - public static final NetworkConfig SECURE_COAP_CONFIG = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(SECURE_PORT)); - public static final String ENDPOINT_SECURITY = "deviceAEndpoint"; - public static final String SECURE_URI = "coaps://localhost:" + SECURE_PORT; - public static final int PORT = 5685; public static final int PORT_BS = 5687; + public static final String HOST = "localhost"; + public static final String HOST_BS = "localhost"; public static final int SHORT_SERVER_ID = 123; public static final int SHORT_SERVER_ID_BS = 111; - public static final Security SECURITY = noSec("coap://localhost:" + PORT, SHORT_SERVER_ID); + public static final NetworkConfig SECURE_COAP_CONFIG = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(SECURE_PORT)); + public static final String SECURE_URI = "coaps://" + HOST + ":" + SECURE_PORT; + public static final Security SECURITY = noSec("coap://"+ HOST +":" + PORT, SHORT_SERVER_ID); public static final NetworkConfig COAP_CONFIG = new NetworkConfig().setString("COAP_PORT", Integer.toString(PORT)); // Models @@ -67,5 +65,4 @@ public class Lwm2mTestHelper { public static final String resourceIdName_3_14 = "UtfOffset"; public static final String resourceIdName_19_0_0 = "dataRead"; public static final String resourceIdName_19_1_0 = "dataWrite"; - } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 95a0a774a2..e3c538c928 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,8 +138,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { -// String endpoint = "WithoutFirmwareInfoDevice"; -// setEndpoint(endpoint); createDeviceProfile(transportConfiguration); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); @@ -165,8 +163,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5() throws Exception { -// String endpoint = "Ota5_Device"; -// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5); final Device device = createDevice(credentials); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 2310fe1659..4303c5e6eb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -18,12 +18,10 @@ package org.thingsboard.server.transport.lwm2m.rpc; import org.junit.Before; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; -import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -53,8 +51,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String RPC_TRANSPORT_CONFIGURATION; - protected ScheduledExecutorService executor; - protected TbTestWebSocketClient wsClient; protected String deviceId; public Set expectedObjects; public Set expectedObjectIdVers; @@ -73,7 +69,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectIdVer_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); - protected static String endpointRpcPref = "deviceEndpointRpc"; + protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; public AbstractRpcLwM2MIntegrationTest(){ setResources(resources); @@ -81,8 +77,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @Before public void beforeTest() throws Exception { - String endpoint = endpointRpcPref + endpointSequence.incrementAndGet(); -// setEndpoint(endpoint); + String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet(); init(); createNewClient (SECURITY, COAP_CONFIG, true, endpoint); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index c10eb46620..c9d2b556af 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -33,36 +33,16 @@ import java.security.cert.X509Certificate; public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { protected final String CREDENTIALS_PATH = "lwm2m/credentials/"; // client public key or id used for PSK - protected final String pskIdentity; // client public key or id used for PSK - protected final String pskKey; // client private/secret key used for PSK -// protected final PublicKey clientPublicKey; // client public key used for RPK -// protected final PrivateKey clientPrivateKey; // client private key used for RPK - - - -// // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) -// protected final X509Certificate clientX509CertWithBadCN; -// // client certificate self-signed with a good CN (CN start by leshan_integration_test) -// protected final X509Certificate clientX509CertSelfSigned; -// // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) -// protected final X509Certificate clientX509CertNotTrusted; - - // self-signed server certificate -// protected final X509Certificate serverX509CertSelfSigned; -// // rootCA used by the server -// protected final X509Certificate rootCAX509Cert; - // certificates trustedby the server (should contain rootCA) + // Get keys PSK + protected final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID"; // client public key or id used for PSK + protected final String CLIENT_PSK_KEY = "73656372657450534b73656372657450"; // client private/secret key used for PSK // Server protected static final String SERVER_JKS_FOR_TEST = "lwm2mserver"; protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_CERT_ALIAS = "server"; - protected final X509Certificate serverX509Cert; // server certificate signed by rootCA -// protected final PrivateKey serverPrivateKeyFromCert; // server private key used for RPK and X509 - protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK - -// // Server Trust -// protected final Certificate[] trustedCertificates = new Certificate[1]; +protected final X509Certificate serverX509Cert; // server certificate signed by rootCA + protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK // Client protected LwM2MTestClient client; @@ -92,45 +72,18 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); -// setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { -// Get keys PSK - this.pskIdentity = "SOME_PSK_ID"; - this.pskKey = "73656372657450534b73656372657450"; - -// // Get point values -// byte[] publicX = Hex -// .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); -// byte[] publicY = Hex -// .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); -// byte[] privateS = Hex -// .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); -// -// // Get Elliptic Curve Parameter spec for secp256r1 -// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); -// algoParameters.init(new ECGenParameterSpec("secp256r1")); -// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); -// -// // Create key specs -// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), -// parameterSpec); -// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); -// -// // Get keys RPK -// clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); -// clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); - // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + CLIENT_JKS_FOR_TEST + ".jks")) { clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - + // Trust clientPrivateKeyFromCertTrust = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST, clientKeyStorePwd); clientX509CertTrust = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST); clientPublicKeyFromCertTrust = clientX509CertTrust != null ? clientX509CertTrust.getPublicKey() : null; - + // No trust clientPrivateKeyFromCertTrustNo = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST_NO, clientKeyStorePwd); clientX509CertTrustNo = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST_NO); clientPublicKeyFromCertTrustNo = clientX509CertTrustNo != null ? clientX509CertTrustNo.getPublicKey() : null; @@ -141,29 +94,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // create server credentials try { -// // Get point values -// byte[] publicX = Hex -// .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); -// byte[] publicY = Hex -// .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); -// byte[] privateS = Hex -// .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); -// -// // Get Elliptic Curve Parameter spec for secp256r1 -// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); -// algoParameters.init(new ECGenParameterSpec("secp256r1")); -// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); -// -// // Create key specs -// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), -// parameterSpec); -// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); -// -// // Get keys -// serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); -// serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); - - // Get certificates from key store char[] serverKeyStorePwd = SERVER_STORE_PWD.toCharArray(); KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -171,13 +101,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M serverKeyStore.load(serverKeyStoreFile, serverKeyStorePwd); } -// serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); serverX509Cert = (X509Certificate) serverKeyStore.getCertificate(SERVER_CERT_ALIAS); serverPublicKeyFromCert = serverX509Cert.getPublicKey(); -// rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); - -// serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); -// trustedCertificates[0] = serverX509Cert; } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 8331b99fff..c3af57ae71 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import lombok.extern.slf4j.Slf4j; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index d7296fd47c..52a85ecf2a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.util.Hex; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -35,12 +34,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); - clientCredentials.setKey(pskKey); - clientCredentials.setIdentity(pskIdentity); + clientCredentials.setKey(CLIENT_PSK_KEY); + clientCredentials.setIdentity(CLIENT_PSK_IDENTITY); Security security = psk(SECURE_URI, SHORT_SERVER_ID, - pskIdentity.getBytes(StandardCharsets.UTF_8), - Hex.decodeHex(pskKey.toCharArray())); + CLIENT_PSK_IDENTITY.getBytes(StandardCharsets.UTF_8), + Hex.decodeHex(CLIENT_PSK_KEY.toCharArray())); super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 0066014a9b..05933019bf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index b2ce6c470d..af7282bdb7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.security.sql; -import org.apache.commons.codec.binary.Base64; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.common.transport.util.SslUtil; @@ -34,7 +32,6 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg public void testConnectWithCertAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST_NO); -// rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); credentials.setCert(SslUtil.getCertificateString(clientX509CertTrustNo)); Security security = x509(SECURE_URI, SHORT_SERVER_ID, @@ -43,5 +40,4 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg serverX509Cert.getEncoded()); super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST_NO); } - } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 175eda993c..d3301bf660 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,6 @@ - From d3987d1c670350d68cdf8578ad54051c897b0869 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 12:58:32 +0200 Subject: [PATCH 059/403] device actor checkSessionsTimeout refactored to improve performance and reduce memory pressure --- .../device/DeviceActorMessageProcessor.java | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 369d371286..d8f0c38975 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -97,6 +97,7 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -116,6 +117,7 @@ import java.util.stream.Collectors; @Slf4j class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { + static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; final DeviceId deviceId; final LinkedHashMapRemoveEldest sessions; @@ -961,19 +963,42 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } void checkSessionsTimeout() { - log.debug("[{}] checkSessionsTimeout started. Size before check {}", deviceId, sessions.size()); - long expTime = System.currentTimeMillis() - systemContext.getSessionInactivityTimeout(); - Map sessionsToRemove = sessions.entrySet().stream().filter(kv -> kv.getValue().getLastActivityTime() < expTime).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - sessionsToRemove.forEach((sessionId, sessionMD) -> { - sessions.remove(sessionId); - rpcSubscriptions.remove(sessionId); - attributeSubscriptions.remove(sessionId); - notifyTransportAboutClosedSession(sessionId, sessionMD, "session timeout!"); - }); - if (!sessionsToRemove.isEmpty()) { - dumpSessions(); + final long expTime = System.currentTimeMillis() - systemContext.getSessionInactivityTimeout(); + List expiredIds = null; + + try { + for (Map.Entry kv : sessions.entrySet()) { //entry set are cached for stable sessions + if (kv.getValue().getLastActivityTime() < expTime) { + final UUID id = kv.getKey(); + if (expiredIds == null) { + expiredIds = new ArrayList<>(1); //most of the expired sessions is a single event + } + expiredIds.add(id); + + } + } + } catch (ConcurrentModificationException ignored) { + //Sessions are not thread safe and possible exceptions + //It is an extremely rare event + //Complete session check will perform on the next check + } + + if (expiredIds != null) { + int removed = 0; + for (UUID id : expiredIds) { + final SessionInfoMetaData session = sessions.remove(id); + rpcSubscriptions.remove(id); + attributeSubscriptions.remove(id); + if (session != null) { + removed++; + notifyTransportAboutClosedSession(id, session, SESSION_TIMEOUT_MESSAGE); + } + } + if (removed != 0) { + dumpSessions(); + } } - log.debug("[{}] checkSessionsTimeout finished. Size after check {}", deviceId, sessions.size()); + } } From 6f19efd5397279e44ea76d5c73edb1136f22fc2d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 13:07:07 +0200 Subject: [PATCH 060/403] TenantId for SYS_TENANT_ID refactored (new instances replaced with constant) --- .../java/org/thingsboard/server/actors/app/AppActor.java | 2 +- .../thingsboard/server/service/install/DatabaseHelper.java | 2 +- .../thingsboard/server/service/install/InstallScripts.java | 4 ++-- .../thingsboard/server/service/mail/DefaultMailService.java | 2 +- .../auth/jwt/RefreshTokenAuthenticationProvider.java | 4 ++-- .../thingsboard/server/service/sms/DefaultSmsService.java | 2 +- .../server/queue/discovery/HashPartitionService.java | 2 +- .../dao/component/BaseComponentDescriptorService.java | 2 +- .../server/dao/device/DeviceCredentialsServiceImpl.java | 2 +- .../thingsboard/server/dao/tenant/TenantServiceImpl.java | 6 +++--- .../thingsboard/server/dao/service/AbstractServiceTest.java | 2 +- .../server/dao/sql/widget/JpaWidgetsBundleDaoTest.java | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 223fb94937..aac0220d07 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -222,7 +222,7 @@ public class AppActor extends ContextAwareActor { @Override public TbActorId createActorId() { - return new TbEntityActorId(new TenantId(EntityId.NULL_UUID)); + return new TbEntityActorId(TenantId.SYS_TENANT_ID); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index f9ea9afa67..44828c3630 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -99,7 +99,7 @@ public class DatabaseHelper { } } for (CustomerId customerId : customerIds) { - dashboardService.assignDashboardToCustomer(new TenantId(EntityId.NULL_UUID), dashboardId, customerId); + dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, dashboardId, customerId); } }); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index d5a4365963..f4e046bcd5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -169,7 +169,7 @@ public class InstallScripts { ruleChain = ruleChainService.saveRuleChain(ruleChain); ruleChainMetaData.setRuleChainId(ruleChain.getId()); - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData); + ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData); return ruleChain; } @@ -217,7 +217,7 @@ public class InstallScripts { dashboard.setTenantId(tenantId); Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); if (customerId != null && !customerId.isNullUid()) { - dashboardService.assignDashboardToCustomer(new TenantId(EntityId.NULL_UUID), savedDashboard.getId(), customerId); + dashboardService.assignDashboardToCustomer(TenantId.SYS_TENANT_ID, savedDashboard.getId(), customerId); } } catch (Exception e) { log.error("Unable to load dashboard from json: [{}]", path.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 0b261436b4..f18fae4ef2 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -95,7 +95,7 @@ public class DefaultMailService implements MailService { @Override public void updateMailConfiguration() { - AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "mail"); + AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); if (settings != null) { JsonNode jsonConfig = settings.getJsonValue(); mailSender = createMailSender(jsonConfig); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 7899c15b70..428fe8adc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -75,7 +75,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } private SecurityUser authenticateByUserId(UserId userId) { - TenantId systemId = new TenantId(EntityId.NULL_UUID); + TenantId systemId = TenantId.SYS_TENANT_ID; User user = userService.findUserById(systemId, userId); if (user == null) { throw new UsernameNotFoundException("User not found by refresh token"); @@ -99,7 +99,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } private SecurityUser authenticateByPublicId(String publicId) { - TenantId systemId = new TenantId(EntityId.NULL_UUID); + TenantId systemId = TenantId.SYS_TENANT_ID; CustomerId customerId; try { customerId = new CustomerId(UUID.fromString(publicId)); diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java index f0b3eb174e..7fdda75144 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java @@ -71,7 +71,7 @@ public class DefaultSmsService implements SmsService { @Override public void updateSmsConfiguration() { - AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms"); + AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms"); if (settings != null) { try { JsonNode jsonConfig = settings.getJsonValue(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 06c635761c..1e657aac29 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -279,7 +279,7 @@ public class HashPartitionService implements PartitionService { tpi.tenantId(tenantId); myPartitionsSearchKey = new ServiceQueueKey(serviceQueue, tenantId); } else { - myPartitionsSearchKey = new ServiceQueueKey(serviceQueue, new TenantId(TenantId.NULL_UUID)); + myPartitionsSearchKey = new ServiceQueueKey(serviceQueue, TenantId.SYS_TENANT_ID); } List partitions = myPartitions.get(myPartitionsSearchKey); if (partitions != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index c00c5fbe56..ac92aa3f67 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -51,7 +51,7 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic @Override public ComponentDescriptor saveComponent(TenantId tenantId, ComponentDescriptor component) { - componentValidator.validate(component, data -> new TenantId(EntityId.NULL_UUID)); + componentValidator.validate(component, data -> TenantId.SYS_TENANT_ID); Optional result = componentDescriptorDao.saveIfNotExist(tenantId, component); return result.orElseGet(() -> componentDescriptorDao.findByClazz(tenantId, component.getClazz())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index e131dd34a4..28ab143d6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -73,7 +73,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen public DeviceCredentials findDeviceCredentialsByCredentialsId(String credentialsId) { log.trace("Executing findDeviceCredentialsByCredentialsId [{}]", credentialsId); validateString(credentialsId, "Incorrect credentialsId " + credentialsId); - return deviceCredentialsDao.findByCredentialsId(new TenantId(EntityId.NULL_UUID), credentialsId); + return deviceCredentialsDao.findByCredentialsId(TenantId.SYS_TENANT_ID, credentialsId); } @Override 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 7314f54b39..4ccaa4e5c9 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 @@ -168,20 +168,20 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe public PageData findTenants(PageLink pageLink) { log.trace("Executing findTenants pageLink [{}]", pageLink); Validator.validatePageLink(pageLink); - return tenantDao.findTenantsByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); + return tenantDao.findTenantsByRegion(TenantId.SYS_TENANT_ID, DEFAULT_TENANT_REGION, pageLink); } @Override public PageData findTenantInfos(PageLink pageLink) { log.trace("Executing findTenantInfos pageLink [{}]", pageLink); Validator.validatePageLink(pageLink); - return tenantDao.findTenantInfosByRegion(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION, pageLink); + return tenantDao.findTenantInfosByRegion(TenantId.SYS_TENANT_ID, DEFAULT_TENANT_REGION, pageLink); } @Override public void deleteTenants() { log.trace("Executing deleteTenants"); - tenantsRemover.removeEntities(new TenantId(EntityId.NULL_UUID), DEFAULT_TENANT_REGION); + tenantsRemover.removeEntities(TenantId.SYS_TENANT_ID, DEFAULT_TENANT_REGION); } private DataValidator tenantValidator = diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 2d4fd72179..c99beb9251 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -86,7 +86,7 @@ public abstract class AbstractServiceTest { protected ObjectMapper mapper = new ObjectMapper(); - public static final TenantId SYSTEM_TENANT_ID = new TenantId(EntityId.NULL_UUID); + public static final TenantId SYSTEM_TENANT_ID = TenantId.SYS_TENANT_ID; @Autowired protected UserService userService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java index 7aa1521978..11e2a47af0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java @@ -158,7 +158,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setAlias(prefix + i); widgetsBundle.setTitle(prefix + i); - widgetsBundle.setTenantId(new TenantId(NULL_UUID)); + widgetsBundle.setTenantId(TenantId.SYS_TENANT_ID); widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); widgetsBundleDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, widgetsBundle); } From 48ac1a256e102bcb9bc62a60b3a2ffbf2a6d05a8 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 13:39:54 +0200 Subject: [PATCH 061/403] TenantId factory method fromUUID added to reduce optimize TenantId instances count in heap memory. Factory as default @JsonCreator --- .../server/common/data/id/TenantId.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index a785ac03d5..efba010507 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -15,23 +15,31 @@ */ package org.thingsboard.server.common.data.id; -import java.util.UUID; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModelProperty; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.util.UUID; + public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore public static final TenantId SYS_TENANT_ID = new TenantId(EntityId.NULL_UUID); + static final ConcurrentReferenceHashMap tenants = new ConcurrentReferenceHashMap<>(16, ReferenceType.SOFT); + private static final long serialVersionUID = 1L; @JsonCreator - public TenantId(@JsonProperty("id") UUID id) { + public static TenantId fromUUID(@JsonProperty("id") UUID id) { + return tenants.computeIfAbsent(id, TenantId::new); + } + + public TenantId(UUID id) { super(id); } From 7d78437258ff55f4dc94ea0dfb3f25a1154ca333 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 13:55:25 +0200 Subject: [PATCH 062/403] TenantId new instance replaced with factory method call fromUUID --- .../server/actors/app/AppActor.java | 2 +- .../server/controller/BaseController.java | 2 +- .../controller/DashboardController.java | 2 +- .../server/controller/DeviceController.java | 2 +- .../server/controller/EventController.java | 6 +++--- .../server/controller/TenantController.java | 6 +++--- .../server/controller/UserController.java | 2 +- .../controller/WidgetTypeController.java | 2 +- .../DefaultTbApiUsageStateService.java | 2 +- .../edge/DefaultEdgeNotificationService.java | 2 +- .../rpc/processor/TelemetryEdgeProcessor.java | 4 ++-- .../ota/DefaultOtaPackageStateService.java | 2 +- .../queue/DefaultTbClusterService.java | 2 +- .../queue/DefaultTbCoreConsumerService.java | 12 +++++------ .../DefaultTbRuleEngineConsumerService.java | 6 +++--- ...lByTenantIdTbRuleEngineSubmitStrategy.java | 2 +- .../service/security/AccessValidator.java | 2 +- .../security/model/token/JwtTokenFactory.java | 2 +- .../state/DefaultDeviceStateService.java | 2 +- .../DefaultRuleEngineStatisticsService.java | 2 +- .../subscription/TbSubscriptionUtils.java | 6 +++--- .../transport/DefaultTransportApiService.java | 8 ++++---- .../msg/TransportToDeviceActorMsgWrapper.java | 2 +- .../sql/BaseTbResourceServiceTest.java | 2 +- .../common/data/id/EntityIdFactory.java | 2 +- .../server/common/data/id/TenantId.java | 3 ++- .../DefaultTbServiceInfoProvider.java | 2 +- .../queue/discovery/HashPartitionService.java | 2 +- .../lwm2m/server/client/LwM2mClient.java | 2 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 4 ++-- .../service/DefaultTransportService.java | 12 +++++------ .../server/dao/entity/BaseEntityService.java | 2 +- .../server/dao/model/ModelConstants.java | 2 +- .../dao/model/sql/AbstractAlarmEntity.java | 2 +- .../dao/model/sql/AbstractAssetEntity.java | 2 +- .../dao/model/sql/AbstractDeviceEntity.java | 2 +- .../dao/model/sql/AbstractEdgeEntity.java | 2 +- .../model/sql/AbstractEntityViewEntity.java | 2 +- .../dao/model/sql/AbstractTenantEntity.java | 2 +- .../model/sql/AbstractWidgetTypeEntity.java | 2 +- .../dao/model/sql/ApiUsageStateEntity.java | 2 +- .../server/dao/model/sql/AuditLogEntity.java | 2 +- .../server/dao/model/sql/CustomerEntity.java | 2 +- .../server/dao/model/sql/DashboardEntity.java | 2 +- .../dao/model/sql/DashboardInfoEntity.java | 2 +- .../dao/model/sql/DeviceProfileEntity.java | 2 +- .../server/dao/model/sql/EdgeEventEntity.java | 2 +- .../dao/model/sql/EntityAlarmEntity.java | 2 +- .../server/dao/model/sql/EventEntity.java | 2 +- .../dao/model/sql/OAuth2ParamsEntity.java | 2 +- .../dao/model/sql/OtaPackageEntity.java | 2 +- .../dao/model/sql/OtaPackageInfoEntity.java | 2 +- .../server/dao/model/sql/RpcEntity.java | 2 +- .../server/dao/model/sql/RuleChainEntity.java | 2 +- .../dao/model/sql/TbResourceEntity.java | 2 +- .../dao/model/sql/TbResourceInfoEntity.java | 2 +- .../server/dao/model/sql/UserEntity.java | 2 +- .../dao/model/sql/WidgetsBundleEntity.java | 2 +- .../dao/resource/BaseResourceService.java | 2 +- .../server/dao/sql/asset/JpaAssetDao.java | 2 +- .../server/dao/sql/device/JpaDeviceDao.java | 2 +- .../server/dao/sql/edge/JpaEdgeDao.java | 2 +- .../dao/sql/entityview/JpaEntityViewDao.java | 2 +- .../dao/sql/query/AlarmDataAdapter.java | 2 +- .../server/dao/sql/tenant/JpaTenantDao.java | 2 +- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../server/dao/user/UserServiceImpl.java | 2 +- .../dao/widget/WidgetTypeServiceImpl.java | 2 +- .../dao/widget/WidgetsBundleServiceImpl.java | 2 +- .../nosql/CassandraPartitionsCacheTest.java | 2 +- .../dao/service/AbstractServiceTest.java | 2 +- .../dao/service/BaseAssetServiceTest.java | 2 +- .../dao/service/BaseCustomerServiceTest.java | 2 +- .../dao/service/BaseDashboardServiceTest.java | 2 +- .../dao/service/BaseDeviceServiceTest.java | 2 +- .../dao/service/BaseEdgeEventServiceTest.java | 6 +++--- .../dao/service/BaseEdgeServiceTest.java | 2 +- .../service/BaseOtaPackageServiceTest.java | 2 +- .../dao/service/BaseRuleChainServiceTest.java | 2 +- .../service/BaseWidgetTypeServiceTest.java | 4 ++-- .../service/BaseWidgetsBundleServiceTest.java | 12 +++++------ .../service/event/BaseEventServiceTest.java | 4 ++-- .../server/dao/sql/alarm/JpaAlarmDaoTest.java | 8 ++++---- .../server/dao/sql/asset/JpaAssetDaoTest.java | 6 +++--- .../dao/sql/customer/JpaCustomerDaoTest.java | 4 ++-- .../dashboard/JpaDashboardInfoDaoTest.java | 2 +- .../dao/sql/device/JpaDeviceDaoTest.java | 20 +++++++++---------- .../dao/sql/event/JpaBaseEventDaoTest.java | 10 +++++----- .../dao/sql/tenant/JpaTenantDaoTest.java | 2 +- .../server/dao/sql/user/JpaUserDaoTest.java | 4 ++-- .../sql/widget/JpaWidgetsBundleDaoTest.java | 2 +- .../engine/action/TbCreateRelationNode.java | 2 +- .../rule/engine/action/TbAlarmNodeTest.java | 2 +- .../metadata/AbstractAttributeNodeTest.java | 2 +- .../profile/TbDeviceProfileNodeTest.java | 2 +- 95 files changed, 148 insertions(+), 147 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index aac0220d07..d64456fba8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -160,7 +160,7 @@ public class AppActor extends ContextAwareActor { } } else { if (EntityType.TENANT.equals(msg.getEntityId().getEntityType())) { - TenantId tenantId = new TenantId(msg.getEntityId().getId()); + TenantId tenantId = TenantId.fromUUID(msg.getEntityId().getId()); if (msg.getEvent() == ComponentLifecycleEvent.DELETED) { log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg); deletedTenants.add(tenantId); 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 c077d5aef5..533d9bb9d1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -482,7 +482,7 @@ public abstract class BaseController { checkCustomerId(new CustomerId(entityId.getId()), operation); return; case TENANT: - checkTenantId(new TenantId(entityId.getId()), operation); + checkTenantId(TenantId.fromUUID(entityId.getId()), operation); return; case TENANT_PROFILE: checkTenantProfileId(new TenantProfileId(entityId.getId()), operation); diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 1d3265053d..bc532c183a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -585,7 +585,7 @@ public class DashboardController extends BaseController { @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); checkTenantId(tenantId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 6ea6bdd880..38239c9a8b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -807,7 +807,7 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId, Operation.ASSIGN_TO_TENANT); - TenantId newTenantId = new TenantId(toUUID(strTenantId)); + TenantId newTenantId = TenantId.fromUUID(toUUID(strTenantId)); Tenant newTenant = tenantService.findTenantById(newTenantId); if (newTenant == null) { throw new ThingsboardException("Could not find the specified Tenant!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index ea77fce94c..5819ac3cb7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -136,7 +136,7 @@ public class EventController extends BaseController { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); @@ -177,7 +177,7 @@ public class EventController extends BaseController { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); @@ -224,7 +224,7 @@ public class EventController extends BaseController { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 0cfc8f3865..72f5422a0b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -82,7 +82,7 @@ public class TenantController extends BaseController { @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); Tenant tenant = checkTenantId(tenantId, Operation.READ); if (!tenant.getAdditionalInfo().isNull()) { processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD); @@ -104,7 +104,7 @@ public class TenantController extends BaseController { @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); return checkTenantInfoId(tenantId, Operation.READ); } catch (Exception e) { throw handleException(e); @@ -154,7 +154,7 @@ public class TenantController extends BaseController { @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); Tenant tenant = checkTenantId(tenantId, Operation.DELETE); tenantService.deleteTenant(tenantId); tenantProfileCache.evict(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e83bc8970c..6007d1831e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -371,7 +371,7 @@ public class UserController extends BaseController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("tenantId", strTenantId); try { - TenantId tenantId = new TenantId(toUUID(strTenantId)); + TenantId tenantId = TenantId.fromUUID(toUUID(strTenantId)); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(userService.findTenantAdmins(tenantId, pageLink)); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index b7f8b28b28..5f7c9ec6c9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -216,7 +216,7 @@ public class WidgetTypeController extends BaseController { try { TenantId tenantId; if (isSystem) { - tenantId = new TenantId(ModelConstants.NULL_UUID); + tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); } else { tenantId = getCurrentUser().getTenantId(); } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index bb768a15d7..9a9b0ff6a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -164,7 +164,7 @@ public class DefaultTbApiUsageStateService extends TbApplicationEventListener msg, TbCallback callback) { ToUsageStatsServiceMsg statsMsg = msg.getValue(); - TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); EntityId entityId; if (statsMsg.getCustomerIdMSB() != 0 && statsMsg.getCustomerIdLSB() != 0) { entityId = new CustomerId(new UUID(statsMsg.getCustomerIdMSB(), statsMsg.getCustomerIdLSB())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index d25e63183c..0ae07631b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -124,7 +124,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) { log.trace("Pushing notification to edge {}", edgeNotificationMsg); try { - TenantId tenantId = new TenantId(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB())); EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); switch (type) { case EDGE: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java index 7aea556544..16af076b3a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java @@ -276,7 +276,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { case DASHBOARD: return new DashboardId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); case TENANT: - return new TenantId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); + return TenantId.fromUUID(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); case CUSTOMER: return new CustomerId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); case USER: @@ -303,7 +303,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { entityId = new DashboardId(edgeEvent.getEntityId()); break; case TENANT: - entityId = new TenantId(edgeEvent.getEntityId()); + entityId = TenantId.fromUUID(edgeEvent.getEntityId()); break; case CUSTOMER: entityId = new CustomerId(edgeEvent.getEntityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java index 883745e643..bfb46ea75d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java @@ -206,7 +206,7 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { boolean isSuccess = false; OtaPackageId targetOtaPackageId = new OtaPackageId(new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB())); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); - TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); OtaPackageType firmwareType = OtaPackageType.valueOf(msg.getType()); long ts = msg.getTs(); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 0a82994a42..834a59811f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -140,7 +140,7 @@ public class DefaultTbClusterService implements TbClusterService { public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg tbMsg, TbQueueCallback callback) { if (tenantId.isNullUid()) { if (entityId.getEntityType().equals(EntityType.TENANT)) { - tenantId = new TenantId(entityId.getId()); + tenantId = TenantId.fromUUID(entityId.getId()); } else { log.warn("[{}][{}] Received invalid message: {}", tenantId, entityId, tbMsg); return; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 3602aa804e..88649272be 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -453,37 +453,37 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService msg) { log.trace("[{}] Creating callback for topic {} message: {}", id, configuration.getName(), msg.getValue()); ToRuleEngineMsg toRuleEngineMsg = msg.getValue(); - TenantId tenantId = new TenantId(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(toRuleEngineMsg.getTenantIdMSB(), toRuleEngineMsg.getTenantIdLSB())); TbMsgCallback callback = prometheusStatsEnabled ? new TbMsgPackCallback(id, tenantId, ctx, stats.getTimer(tenantId, SUCCESSFUL_STATUS), stats.getTimer(tenantId, FAILED_STATUS)) : new TbMsgPackCallback(id, tenantId, ctx); @@ -344,9 +344,9 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< TbMsg tmpMsg = TbMsg.fromBytes(configuration.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { - log.trace("[{}] {} to process message: {}, Last Rule Node: {}", new TenantId(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); + log.trace("[{}] {} to process message: {}, Last Rule Node: {}", TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); } else { - log.info("[{}] {} to process message: {}, Last Rule Node: {}", new TenantId(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); + log.info("[{}] {} to process message: {}, Last Rule Node: {}", TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); break; } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java index 7c7d076d94..fba60ea6fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByTenantIdTbRuleEngineSubmitStrategy.java @@ -29,6 +29,6 @@ public class SequentialByTenantIdTbRuleEngineSubmitStrategy extends SequentialBy @Override protected EntityId getEntityId(TransportProtos.ToRuleEngineMsg msg) { - return new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + return TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); } } 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 ab9d0de231..829ef08865 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 @@ -450,7 +450,7 @@ public class AccessValidator { } else if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.ok(null)); } else { - ListenableFuture tenantFuture = tenantService.findTenantByIdAsync(currentUser.getTenantId(), new TenantId(entityId.getId())); + ListenableFuture tenantFuture = tenantService.findTenantByIdAsync(currentUser.getTenantId(), TenantId.fromUUID(entityId.getId())); Futures.addCallback(tenantFuture, getCallback(callback, tenant -> { if (tenant == null) { return ValidationResult.entityNotFound("Tenant with requested id wasn't found!"); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 9315b3bcc1..f8afd4932c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -126,7 +126,7 @@ public class JwtTokenFactory { securityUser.setUserPrincipal(principal); String tenantId = claims.get(TENANT_ID, String.class); if (tenantId != null) { - securityUser.setTenantId(new TenantId(UUID.fromString(tenantId))); + securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId))); } String customerId = claims.get(CUSTOMER_ID, String.class); if (customerId != null) { diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 97c22d69c9..2c68414369 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -237,7 +237,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener { - TenantId tenantId = new TenantId(id); + TenantId tenantId = TenantId.fromUUID(id); try { AssetId serviceAssetId = getServiceAssetId(tenantId, queueName); if (stats.getTotalMsgCounter().get() > 0) { diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 82e0b17f67..023e961451 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -125,7 +125,7 @@ public class TbSubscriptionUtils { .sessionId(subProto.getSessionId()) .subscriptionId(subProto.getSubscriptionId()) .entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) - .tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); + .tenantId(TenantId.fromUUID(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); builder.scope(TbAttributeSubscriptionScope.valueOf(attributeSub.getScope())); builder.allKeys(attributeSub.getAllKeys()); @@ -142,7 +142,7 @@ public class TbSubscriptionUtils { .sessionId(subProto.getSessionId()) .subscriptionId(subProto.getSubscriptionId()) .entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) - .tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); + .tenantId(TenantId.fromUUID(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); builder.allKeys(telemetrySub.getAllKeys()); Map keyStates = new HashMap<>(); @@ -161,7 +161,7 @@ public class TbSubscriptionUtils { .sessionId(subProto.getSessionId()) .subscriptionId(subProto.getSubscriptionId()) .entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) - .tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); + .tenantId(TenantId.fromUUID(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); builder.ts(alarmSub.getTs()); return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 1cefefd5d1..80fb062c90 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -380,7 +380,7 @@ public class DefaultTransportApiService implements TransportApiService { DeviceProfile deviceProfile = deviceProfileCache.find(deviceProfileId); builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile))); } else if (entityType.equals(EntityType.TENANT)) { - TenantId tenantId = new TenantId(entityUuid); + TenantId tenantId = TenantId.fromUUID(entityUuid); TenantProfile tenantProfile = tenantProfileCache.get(tenantId); ApiUsageState state = apiUsageStateService.getApiUsageState(tenantId); builder.setData(ByteString.copyFrom(dataDecodingEncodingService.encode(tenantProfile))); @@ -425,7 +425,7 @@ public class DefaultTransportApiService implements TransportApiService { private ListenableFuture handle(GetResourceRequestMsg requestMsg) { - TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(requestMsg.getResourceType()); String resourceKey = requestMsg.getResourceKey(); TransportProtos.GetResourceResponseMsg.Builder builder = TransportProtos.GetResourceResponseMsg.newBuilder(); @@ -542,7 +542,7 @@ public class DefaultTransportApiService implements TransportApiService { } private ListenableFuture handle(TransportProtos.GetOtaPackageRequestMsg requestMsg) { - TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(requestMsg.getDeviceIdMSB(), requestMsg.getDeviceIdLSB())); OtaPackageType otaPackageType = OtaPackageType.valueOf(requestMsg.getType()); Device device = deviceService.findDeviceById(tenantId, deviceId); @@ -592,7 +592,7 @@ public class DefaultTransportApiService implements TransportApiService { } private ListenableFuture handleRegistration(TransportProtos.LwM2MRegistrationRequestMsg msg) { - TenantId tenantId = new TenantId(UUID.fromString(msg.getTenantId())); + TenantId tenantId = TenantId.fromUUID(UUID.fromString(msg.getTenantId())); String deviceName = msg.getEndpoint(); Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(deviceName, id -> new ReentrantLock()); deviceCreationLock.lock(); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java index 74d81c0307..e2aba53bf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java @@ -44,7 +44,7 @@ public class TransportToDeviceActorMsgWrapper implements TbActorMsg, DeviceAware public TransportToDeviceActorMsgWrapper(TransportToDeviceActorMsg msg, TbCallback callback) { this.msg = msg; this.callback = callback; - this.tenantId = new TenantId(new UUID(msg.getSessionInfo().getTenantIdMSB(), msg.getSessionInfo().getTenantIdLSB())); + this.tenantId = TenantId.fromUUID(new UUID(msg.getSessionInfo().getTenantIdMSB(), msg.getSessionInfo().getTenantIdLSB())); this.deviceId = new DeviceId(new UUID(msg.getSessionInfo().getDeviceIdMSB(), msg.getSessionInfo().getDeviceIdLSB())); } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index caf4431e7d..035dc82e75 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -277,7 +277,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { @Test(expected = DataValidationException.class) public void testSaveTbResourceWithInvalidTenant() throws Exception { TbResource resource = new TbResource(); - resource.setTenantId(new TenantId(Uuids.timeBased())); + resource.setTenantId(TenantId.fromUUID(Uuids.timeBased())); resource.setResourceType(ResourceType.JKS); resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); 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 6d8ddfdd18..519ac4d4b8 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 @@ -40,7 +40,7 @@ public class EntityIdFactory { public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { switch (type) { case TENANT: - return new TenantId(uuid); + return TenantId.fromUUID(uuid); case CUSTOMER: return new CustomerId(uuid); case USER: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index efba010507..665edc0ca9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -28,7 +28,7 @@ import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore - public static final TenantId SYS_TENANT_ID = new TenantId(EntityId.NULL_UUID); + public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); static final ConcurrentReferenceHashMap tenants = new ConcurrentReferenceHashMap<>(16, ReferenceType.SOFT); @@ -39,6 +39,7 @@ public final class TenantId extends UUIDBased implements EntityId { return tenants.computeIfAbsent(id, TenantId::new); } + //default constructor is still available due to possible usage in extensions public TenantId(UUID id) { super(id); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index 6988bef9e1..b94c009ad7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -88,7 +88,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { UUID tenantId; if (!StringUtils.isEmpty(tenantIdStr)) { tenantId = UUID.fromString(tenantIdStr); - isolatedTenant = new TenantId(tenantId); + isolatedTenant = TenantId.fromUUID(tenantId); } else { tenantId = TenantId.NULL_UUID; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 1e657aac29..59693732c8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -327,7 +327,7 @@ public class HashPartitionService implements PartitionService { } private TenantId getSystemOrIsolatedTenantId(TransportProtos.ServiceInfo serviceInfo) { - return new TenantId(new UUID(serviceInfo.getTenantIdMSB(), serviceInfo.getTenantIdLSB())); + return TenantId.fromUUID(new UUID(serviceInfo.getTenantIdMSB(), serviceInfo.getTenantIdLSB())); } private void addNode(Map> queueServiceList, ServiceInfo instance) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index e1565d5be2..83fdf51787 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -148,7 +148,7 @@ public class LwM2mClient implements Serializable { public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) { this.session = createSession(nodeId, sessionId, credentials); - this.tenantId = new TenantId(new UUID(session.getTenantIdMSB(), session.getTenantIdLSB())); + this.tenantId = TenantId.fromUUID(new UUID(session.getTenantIdMSB(), session.getTenantIdLSB())); this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB()); this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB()); this.powerMode = credentials.getDeviceInfo().getPowerMode(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index db1390768e..6a31a650df 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -428,7 +428,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt) { String idVer = resourceUpdateMsgOpt.getResourceKey(); - TenantId tenantId = new TenantId(new UUID(resourceUpdateMsgOpt.getTenantIdMSB(), resourceUpdateMsgOpt.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(resourceUpdateMsgOpt.getTenantIdMSB(), resourceUpdateMsgOpt.getTenantIdLSB())); modelProvider.evict(tenantId, idVer); clientContext.getLwM2mClients().forEach(e -> e.updateResourceModel(idVer, modelProvider)); } @@ -436,7 +436,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceDeleteMsgOpt) { String pathIdVer = resourceDeleteMsgOpt.getResourceKey(); - TenantId tenantId = new TenantId(new UUID(resourceDeleteMsgOpt.getTenantIdMSB(), resourceDeleteMsgOpt.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(resourceDeleteMsgOpt.getTenantIdMSB(), resourceDeleteMsgOpt.getTenantIdLSB())); modelProvider.evict(tenantId, pathIdVer); clientContext.getLwM2mClients().forEach(e -> e.deleteResources(pathIdVer, modelProvider)); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 36e1a68daa..57d1da63b4 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -451,7 +451,7 @@ public class DefaultTransportService implements TransportService { private TransportDeviceInfo getTransportDeviceInfo(TransportProtos.DeviceInfoProto di) { TransportDeviceInfo tdi = new TransportDeviceInfo(); - tdi.setTenantId(new TenantId(new UUID(di.getTenantIdMSB(), di.getTenantIdLSB()))); + tdi.setTenantId(TenantId.fromUUID(new UUID(di.getTenantIdMSB(), di.getTenantIdLSB()))); tdi.setCustomerId(new CustomerId(new UUID(di.getCustomerIdMSB(), di.getCustomerIdLSB()))); tdi.setDeviceId(new DeviceId(new UUID(di.getDeviceIdMSB(), di.getDeviceIdLSB()))); tdi.setDeviceProfileId(new DeviceProfileId(new UUID(di.getDeviceProfileIdMSB(), di.getDeviceProfileIdLSB()))); @@ -805,7 +805,7 @@ public class DefaultTransportService implements TransportService { if (log.isTraceEnabled()) { log.trace("[{}] Processing msg: {}", toSessionId(sessionInfo), msg); } - TenantId tenantId = new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); DeviceId deviceId = new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); EntityType rateLimitedEntityType = rateLimitService.checkLimits(tenantId, deviceId, dataPoints); @@ -895,14 +895,14 @@ public class DefaultTransportService implements TransportService { } else if (EntityType.TENANT_PROFILE.equals(entityType)) { tenantProfileCache.remove(new TenantProfileId(entityUuid)); } else if (EntityType.TENANT.equals(entityType)) { - rateLimitService.remove(new TenantId(entityUuid)); + rateLimitService.remove(TenantId.fromUUID(entityUuid)); } else if (EntityType.DEVICE.equals(entityType)) { rateLimitService.remove(new DeviceId(entityUuid)); onDeviceDeleted(new DeviceId(entityUuid)); } } else if (toSessionMsg.hasResourceUpdateMsg()) { TransportProtos.ResourceUpdateMsg msg = toSessionMsg.getResourceUpdateMsg(); - TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); String resourceId = msg.getResourceKey(); transportResourceCache.update(tenantId, resourceType, resourceId); @@ -913,7 +913,7 @@ public class DefaultTransportService implements TransportService { } else if (toSessionMsg.hasResourceDeleteMsg()) { TransportProtos.ResourceDeleteMsg msg = toSessionMsg.getResourceDeleteMsg(); - TenantId tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); ResourceType resourceType = ResourceType.valueOf(msg.getResourceType()); String resourceId = msg.getResourceKey(); transportResourceCache.evict(tenantId, resourceType, resourceId); @@ -1001,7 +1001,7 @@ public class DefaultTransportService implements TransportService { } protected TenantId getTenantId(TransportProtos.SessionInfoProto sessionInfo) { - return new TenantId(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); + return TenantId.fromUUID(new UUID(sessionInfo.getTenantIdMSB(), sessionInfo.getTenantIdLSB())); } protected CustomerId getCustomerId(TransportProtos.SessionInfoProto sessionInfo) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 06e19a7730..948ed549f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -139,7 +139,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe hasName = entityViewService.findEntityViewByIdAsync(tenantId, new EntityViewId(entityId.getId())); break; case TENANT: - hasName = tenantService.findTenantByIdAsync(tenantId, new TenantId(entityId.getId())); + hasName = tenantService.findTenantByIdAsync(tenantId, TenantId.fromUUID(entityId.getId())); break; case CUSTOMER: hasName = customerService.findCustomerByIdAsync(tenantId, new CustomerId(entityId.getId())); 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 6f7a3c2567..ec25ed26a7 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 @@ -28,7 +28,7 @@ public class ModelConstants { } public static final UUID NULL_UUID = Uuids.startOf(0); - public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID); + public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID); // this is the difference between midnight October 15, 1582 UTC and midnight January 1, 1970 UTC as 100 nanosecond units public static final long EPOCH_DIFF = 122192928000000000L; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index 8ade2b78d5..afa5d06215 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -166,7 +166,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity Alarm alarm = new Alarm(new AlarmId(id)); alarm.setCreatedTime(createdTime); if (tenantId != null) { - alarm.setTenantId(new TenantId(tenantId)); + alarm.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { alarm.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java index 07d24020ff..1984dea60c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAssetEntity.java @@ -119,7 +119,7 @@ public abstract class AbstractAssetEntity extends BaseSqlEntity Asset asset = new Asset(new AssetId(id)); asset.setCreatedTime(createdTime); if (tenantId != null) { - asset.setTenantId(new TenantId(tenantId)); + asset.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { asset.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java index c9913565e4..14a71cad48 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -145,7 +145,7 @@ public abstract class AbstractDeviceEntity extends BaseSqlEnti Device device = new Device(new DeviceId(getUuid())); device.setCreatedTime(createdTime); if (tenantId != null) { - device.setTenantId(new TenantId(tenantId)); + device.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { device.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java index bb9e9d98ff..4929e6a8ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractEdgeEntity.java @@ -153,7 +153,7 @@ public abstract class AbstractEdgeEntity extends BaseSqlEntity extends Bas entityView.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType.name(), entityId)); } if (tenantId != null) { - entityView.setTenantId(new TenantId(tenantId)); + entityView.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { entityView.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java index 7ba0fc0871..8445e6c4bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTenantEntity.java @@ -138,7 +138,7 @@ public abstract class AbstractTenantEntity extends BaseSqlEnti } protected Tenant toTenant() { - Tenant tenant = new Tenant(new TenantId(this.getUuid())); + Tenant tenant = new Tenant(TenantId.fromUUID(this.getUuid())); tenant.setCreatedTime(createdTime); tenant.setTitle(title); tenant.setRegion(region); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java index 803d1604f8..52f6f6f486 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java @@ -75,7 +75,7 @@ public abstract class AbstractWidgetTypeEntity extends BaseWidgetType widgetType = new BaseWidgetType(new WidgetTypeId(getUuid())); widgetType.setCreatedTime(createdTime); if (tenantId != null) { - widgetType.setTenantId(new TenantId(tenantId)); + widgetType.setTenantId(TenantId.fromUUID(tenantId)); } widgetType.setBundleAlias(bundleAlias); widgetType.setAlias(alias); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java index 40b56c0095..7919d64eec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -102,7 +102,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements ApiUsageState ur = new ApiUsageState(new ApiUsageStateId(this.getUuid())); ur.setCreatedTime(createdTime); if (tenantId != null) { - ur.setTenantId(new TenantId(tenantId)); + ur.setTenantId(TenantId.fromUUID(tenantId)); } if (entityId != null) { ur.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java index 17e30a817f..b1447467d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java @@ -132,7 +132,7 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit AuditLog auditLog = new AuditLog(new AuditLogId(this.getUuid())); auditLog.setCreatedTime(createdTime); if (tenantId != null) { - auditLog.setTenantId(new TenantId(tenantId)); + auditLog.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { auditLog.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java index 6ed9a3ff51..252cb096a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java @@ -113,7 +113,7 @@ public final class CustomerEntity extends BaseSqlEntity implements Sea public Customer toData() { Customer customer = new Customer(new CustomerId(this.getUuid())); customer.setCreatedTime(createdTime); - customer.setTenantId(new TenantId(tenantId)); + customer.setTenantId(TenantId.fromUUID(tenantId)); customer.setTitle(title); customer.setCountry(country); customer.setState(state); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index 518a45e5fd..5c01f3f3b4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -119,7 +119,7 @@ public final class DashboardEntity extends BaseSqlEntity implements S Dashboard dashboard = new Dashboard(new DashboardId(this.getUuid())); dashboard.setCreatedTime(this.getCreatedTime()); if (tenantId != null) { - dashboard.setTenantId(new TenantId(tenantId)); + dashboard.setTenantId(TenantId.fromUUID(tenantId)); } dashboard.setTitle(title); dashboard.setImage(image); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index 8d202b2dad..a8f9efba4f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -113,7 +113,7 @@ public class DashboardInfoEntity extends BaseSqlEntity implements DashboardInfo dashboardInfo = new DashboardInfo(new DashboardId(this.getUuid())); dashboardInfo.setCreatedTime(createdTime); if (tenantId != null) { - dashboardInfo.setTenantId(new TenantId(tenantId)); + dashboardInfo.setTenantId(TenantId.fromUUID(tenantId)); } dashboardInfo.setTitle(title); dashboardInfo.setImage(image); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index a3d6c491c0..1ae936810e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -158,7 +158,7 @@ public final class DeviceProfileEntity extends BaseSqlEntity impl DeviceProfile deviceProfile = new DeviceProfile(new DeviceProfileId(this.getUuid())); deviceProfile.setCreatedTime(createdTime); if (tenantId != null) { - deviceProfile.setTenantId(new TenantId(tenantId)); + deviceProfile.setTenantId(TenantId.fromUUID(tenantId)); } deviceProfile.setName(name); deviceProfile.setType(type); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java index b30d40470b..90a1f2a137 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java @@ -111,7 +111,7 @@ public class EdgeEventEntity extends BaseSqlEntity implements BaseEnt public EdgeEvent toData() { EdgeEvent edgeEvent = new EdgeEvent(new EdgeEventId(this.getUuid())); edgeEvent.setCreatedTime(createdTime); - edgeEvent.setTenantId(new TenantId(tenantId)); + edgeEvent.setTenantId(TenantId.fromUUID(tenantId)); edgeEvent.setEdgeId(new EdgeId(edgeId)); if (entityId != null) { edgeEvent.setEntityId(entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java index 50d57cf61c..0a2842ca8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java @@ -85,7 +85,7 @@ public final class EntityAlarmEntity implements ToData { @Override public EntityAlarm toData() { EntityAlarm result = new EntityAlarm(); - result.setTenantId(new TenantId(tenantId)); + result.setTenantId(TenantId.fromUUID(tenantId)); result.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); result.setAlarmId(new AlarmId(alarmId)); result.setAlarmType(alarmType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java index 2d1e5b555d..15bb01df6a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EventEntity.java @@ -103,7 +103,7 @@ public class EventEntity extends BaseSqlEntity implements BaseEntity { OAuth2Params oauth2Params = new OAuth2Params(); oauth2Params.setId(new OAuth2ParamsId(id)); oauth2Params.setCreatedTime(createdTime); - oauth2Params.setTenantId(new TenantId(tenantId)); + oauth2Params.setTenantId(TenantId.fromUUID(tenantId)); oauth2Params.setEnabled(enabled); return oauth2Params; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 7e6f9a2aff..e6d14c588d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -151,7 +151,7 @@ public class OtaPackageEntity extends BaseSqlEntity implements Searc public OtaPackage toData() { OtaPackage otaPackage = new OtaPackage(new OtaPackageId(id)); otaPackage.setCreatedTime(createdTime); - otaPackage.setTenantId(new TenantId(tenantId)); + otaPackage.setTenantId(TenantId.fromUUID(tenantId)); if (deviceProfileId != null) { otaPackage.setDeviceProfileId(new DeviceProfileId(deviceProfileId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index b4909a6d2d..1eb94ddc96 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -170,7 +170,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity implemen public OtaPackageInfo toData() { OtaPackageInfo otaPackageInfo = new OtaPackageInfo(new OtaPackageId(id)); otaPackageInfo.setCreatedTime(createdTime); - otaPackageInfo.setTenantId(new TenantId(tenantId)); + otaPackageInfo.setTenantId(TenantId.fromUUID(tenantId)); if (deviceProfileId != null) { otaPackageInfo.setDeviceProfileId(new DeviceProfileId(deviceProfileId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RpcEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RpcEntity.java index d68aa1afb8..aadbf1e686 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RpcEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RpcEntity.java @@ -97,7 +97,7 @@ public class RpcEntity extends BaseSqlEntity implements BaseEntity { public Rpc toData() { Rpc rpc = new Rpc(new RpcId(id)); rpc.setCreatedTime(createdTime); - rpc.setTenantId(new TenantId(tenantId)); + rpc.setTenantId(TenantId.fromUUID(tenantId)); rpc.setDeviceId(new DeviceId(deviceId)); rpc.setExpirationTime(expirationTime); rpc.setRequest(request); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index be195723bd..6c4f85294c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -110,7 +110,7 @@ public class RuleChainEntity extends BaseSqlEntity implements SearchT public RuleChain toData() { RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); ruleChain.setCreatedTime(createdTime); - ruleChain.setTenantId(new TenantId(tenantId)); + ruleChain.setTenantId(TenantId.fromUUID(tenantId)); ruleChain.setName(name); ruleChain.setType(type); if (firstRuleNodeId != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java index 5b22b6e012..206da7bcf3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java @@ -88,7 +88,7 @@ public class TbResourceEntity extends BaseSqlEntity implements Searc public TbResource toData() { TbResource resource = new TbResource(new TbResourceId(id)); resource.setCreatedTime(createdTime); - resource.setTenantId(new TenantId(tenantId)); + resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceKey(resourceKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java index 78c36861c4..d063ececc7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java @@ -76,7 +76,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity implemen public TbResourceInfo toData() { TbResourceInfo resource = new TbResourceInfo(new TbResourceId(id)); resource.setCreatedTime(createdTime); - resource.setTenantId(new TenantId(tenantId)); + resource.setTenantId(TenantId.fromUUID(tenantId)); resource.setTitle(title); resource.setResourceType(ResourceType.valueOf(resourceType)); resource.setResourceKey(resourceKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java index ba6e7ec042..bbe08b7eac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java @@ -110,7 +110,7 @@ public class UserEntity extends BaseSqlEntity implements SearchTextEntity< user.setCreatedTime(createdTime); user.setAuthority(authority); if (tenantId != null) { - user.setTenantId(new TenantId(tenantId)); + user.setTenantId(TenantId.fromUUID(tenantId)); } if (customerId != null) { user.setCustomerId(new CustomerId(customerId)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java index 205ebf23f9..bd556c9605 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java @@ -87,7 +87,7 @@ public final class WidgetsBundleEntity extends BaseSqlEntity impl WidgetsBundle widgetsBundle = new WidgetsBundle(new WidgetsBundleId(id)); widgetsBundle.setCreatedTime(createdTime); if (tenantId != null) { - widgetsBundle.setTenantId(new TenantId(tenantId)); + widgetsBundle.setTenantId(TenantId.fromUUID(tenantId)); } widgetsBundle.setAlias(alias); widgetsBundle.setTitle(title); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index f442f5acea..d4e5f3b5b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -180,7 +180,7 @@ public class BaseResourceService implements ResourceService { throw new DataValidationException("Resource key should be specified!"); } if (resource.getTenantId() == null) { - resource.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + resource.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); } if (!resource.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { Tenant tenant = tenantDao.findById(tenantId, resource.getTenantId().getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 2f8dbe5b02..9a78ac28e4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -182,7 +182,7 @@ public class JpaAssetDao extends JpaAbstractSearchTextDao im if (types != null && !types.isEmpty()) { list = new ArrayList<>(); for (String type : types) { - list.add(new EntitySubtype(new TenantId(tenantId), EntityType.ASSET, type)); + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ASSET, type)); } } return list; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 163a03857a..76468c4155 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -274,7 +274,7 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao if (types != null && !types.isEmpty()) { list = new ArrayList<>(); for (String type : types) { - list.add(new EntitySubtype(new TenantId(tenantId), EntityType.DEVICE, type)); + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.DEVICE, type)); } } return list; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java index 1049a35775..6365064dde 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaEdgeDao.java @@ -187,7 +187,7 @@ public class JpaEdgeDao extends JpaAbstractSearchTextDao imple if (types != null && !types.isEmpty()) { list = new ArrayList<>(); for (String type : types) { - list.add(new EntitySubtype(new TenantId(tenantId), EntityType.EDGE, type)); + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.EDGE, type)); } } return list; 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 index 35f81a860c..451a141c8d 100644 --- 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 @@ -172,7 +172,7 @@ public class JpaEntityViewDao extends JpaAbstractSearchTextDao(); for (String type : types) { - list.add(new EntitySubtype(new TenantId(tenantId), EntityType.ENTITY_VIEW, type)); + list.add(new EntitySubtype(TenantId.fromUUID(tenantId), EntityType.ENTITY_VIEW, type)); } } return list; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index df2d00fdbb..40a2cb972f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -86,7 +86,7 @@ public class AlarmDataAdapter { alarm.setType(row.get(ModelConstants.ALARM_TYPE_PROPERTY).toString()); alarm.setSeverity(AlarmSeverity.valueOf(row.get(ModelConstants.ALARM_SEVERITY_PROPERTY).toString())); alarm.setStatus(AlarmStatus.valueOf(row.get(ModelConstants.ALARM_STATUS_PROPERTY).toString())); - alarm.setTenantId(new TenantId((UUID) row.get(ModelConstants.TENANT_ID_PROPERTY))); + alarm.setTenantId(TenantId.fromUUID((UUID) row.get(ModelConstants.TENANT_ID_PROPERTY))); Object customerIdObj = row.get(ModelConstants.CUSTOMER_ID_PROPERTY); CustomerId customerId = customerIdObj != null ? new CustomerId((UUID) customerIdObj) : null; alarm.setCustomerId(customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java index ff9dce6c96..d963e57eab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDao.java @@ -77,7 +77,7 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao @Override public PageData findTenantsIds(PageLink pageLink) { - return DaoUtil.pageToPageData(tenantRepository.findTenantsIds(DaoUtil.toPageable(pageLink))).mapData(TenantId::new); + return DaoUtil.pageToPageData(tenantRepository.findTenantsIds(DaoUtil.toPageable(pageLink))).mapData(TenantId::fromUUID); } } 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 4ccaa4e5c9..6b0e1bb182 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 @@ -224,7 +224,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Override protected void removeEntity(TenantId tenantId, Tenant entity) { - deleteTenant(new TenantId(entity.getUuidId())); + deleteTenant(TenantId.fromUUID(entity.getUuidId())); } }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 1ed5811ffa..3b4eb70e41 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -409,7 +409,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } TenantId tenantId = user.getTenantId(); if (tenantId == null) { - tenantId = new TenantId(ModelConstants.NULL_UUID); + tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); user.setTenantId(tenantId); } CustomerId customerId = user.getCustomerId(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index d8a8ea3e35..44850ccd34 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -135,7 +135,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { throw new DataValidationException("Widgets type descriptor can't be empty!"); } if (widgetTypeDetails.getTenantId() == null) { - widgetTypeDetails.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + widgetTypeDetails.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); } if (!widgetTypeDetails.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { Tenant tenant = tenantDao.findById(tenantId, widgetTypeDetails.getTenantId().getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 4f7bd93eca..dd6a1e8961 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -159,7 +159,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { throw new DataValidationException("Widgets bundle title should be specified!"); } if (widgetsBundle.getTenantId() == null) { - widgetsBundle.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + widgetsBundle.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); } if (!widgetsBundle.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { Tenant tenant = tenantDao.findById(tenantId, widgetsBundle.getTenantId().getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java index ef371569dd..cf214990b6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java @@ -96,7 +96,7 @@ public class CassandraPartitionsCacheTest { cassandraBaseTimeseriesDao.init(); UUID id = UUID.randomUUID(); - TenantId tenantId = new TenantId(id); + TenantId tenantId = TenantId.fromUUID(id); long tsKvEntryTs = System.currentTimeMillis(); for (int i = 0; i < 50000; i++) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index c99beb9251..2f524eaa50 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -173,7 +173,7 @@ public abstract class AbstractServiceTest { protected Event generateEvent(TenantId tenantId, EntityId entityId, String eventType, String eventUid) throws IOException { if (tenantId == null) { - tenantId = new TenantId(Uuids.timeBased()); + tenantId = TenantId.fromUUID(Uuids.timeBased()); } Event event = new Event(); event.setTenantId(tenantId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index a4a88d24ea..b7c9feb493 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -104,7 +104,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); - asset.setTenantId(new TenantId(Uuids.timeBased())); + asset.setTenantId(TenantId.fromUUID(Uuids.timeBased())); assetService.saveAsset(asset); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java index 5991f6de2c..3b795fe09f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java @@ -105,7 +105,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); - customer.setTenantId(new TenantId(Uuids.timeBased())); + customer.setTenantId(TenantId.fromUUID(Uuids.timeBased())); customerService.saveCustomer(customer); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index ce307b74b0..15b39b1aa7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -102,7 +102,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { public void testSaveDashboardWithInvalidTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); - dashboard.setTenantId(new TenantId(Uuids.timeBased())); + dashboard.setTenantId(TenantId.fromUUID(Uuids.timeBased())); dashboardService.saveDashboard(dashboard); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 1a9432419e..dfb0434136 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -267,7 +267,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { Device device = new Device(); device.setName("My device"); device.setType("default"); - device.setTenantId(new TenantId(Uuids.timeBased())); + device.setTenantId(TenantId.fromUUID(Uuids.timeBased())); deviceService.saveDevice(device); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java index 51a4975dcd..e5e62384c0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeEventServiceTest.java @@ -53,7 +53,7 @@ public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType edgeEventAction) throws IOException { if (tenantId == null) { - tenantId = new TenantId(Uuids.timeBased()); + tenantId = TenantId.fromUUID(Uuids.timeBased()); } EdgeEvent edgeEvent = new EdgeEvent(); edgeEvent.setTenantId(tenantId); @@ -76,7 +76,7 @@ public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { EdgeId edgeId = new EdgeId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased()); - TenantId tenantId = new TenantId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId); EdgeEvent savedEdgeEvent = saveEdgeEventWithProvidedTime(eventTime, edgeId, deviceId, tenantId); EdgeEvent savedEdgeEvent2 = saveEdgeEventWithProvidedTime(eventTime + 1, edgeId, deviceId, tenantId); @@ -105,7 +105,7 @@ public abstract class BaseEdgeEventServiceTest extends AbstractServiceTest { public void findEdgeEventsWithTsUpdateAndWithout() throws Exception { EdgeId edgeId = new EdgeId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased()); - TenantId tenantId = new TenantId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime", SortOrder.Direction.ASC)); EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 631fcff8da..340afefc6d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -100,7 +100,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { Edge edge = new Edge(); edge.setName("My edge"); edge.setType("default"); - edge.setTenantId(new TenantId(Uuids.timeBased())); + edge.setTenantId(TenantId.fromUUID(Uuids.timeBased())); edgeService.saveEdge(edge, true); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index 8fcadcd812..3dc7130981 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -342,7 +342,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @Test public void testSaveFirmwareWithInvalidTenant() { OtaPackage firmware = new OtaPackage(); - firmware.setTenantId(new TenantId(Uuids.timeBased())); + firmware.setTenantId(TenantId.fromUUID(Uuids.timeBased())); firmware.setDeviceProfileId(deviceProfileId); firmware.setType(FIRMWARE); firmware.setTitle(TITLE); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index c1635a6281..deaab55ac2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -97,7 +97,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { public void testSaveRuleChainWithInvalidTenant() { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); - ruleChain.setTenantId(new TenantId(Uuids.timeBased())); + ruleChain.setTenantId(TenantId.fromUUID(Uuids.timeBased())); ruleChainService.saveRuleChain(ruleChain); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java index ba500f950d..024745dc9e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java @@ -142,7 +142,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); WidgetTypeDetails widgetType = new WidgetTypeDetails(); - widgetType.setTenantId(new TenantId(Uuids.timeBased())); + widgetType.setTenantId(TenantId.fromUUID(Uuids.timeBased())); widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); @@ -176,7 +176,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); - savedWidgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + savedWidgetType.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { widgetTypeService.saveWidgetType(savedWidgetType); } finally { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java index 76f67c3c48..3573fbcaaf 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java @@ -88,7 +88,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { public void testSaveWidgetsBundleWithInvalidTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); - widgetsBundle.setTenantId(new TenantId(Uuids.timeBased())); + widgetsBundle.setTenantId(TenantId.fromUUID(Uuids.timeBased())); widgetsBundleService.saveWidgetsBundle(widgetsBundle); } @@ -98,7 +98,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundle.setTitle("My widgets bundle"); widgetsBundle.setTenantId(tenantId); WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); - savedWidgetsBundle.setTenantId(new TenantId(ModelConstants.NULL_UUID)); + savedWidgetsBundle.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); } finally { @@ -160,7 +160,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { @Test public void testFindSystemWidgetsBundlesByPageLink() { - TenantId tenantId = new TenantId(ModelConstants.NULL_UUID); + TenantId tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); List systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles(tenantId); List createdWidgetsBundles = new ArrayList<>(); @@ -204,7 +204,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { @Test public void testFindSystemWidgetsBundles() { - TenantId tenantId = new TenantId(ModelConstants.NULL_UUID); + TenantId tenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); List systemWidgetsBundles = widgetsBundleService.findSystemWidgetsBundles(tenantId); @@ -290,7 +290,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { tenant = tenantService.saveTenant(tenant); TenantId tenantId = tenant.getId(); - TenantId systemTenantId = new TenantId(ModelConstants.NULL_UUID); + TenantId systemTenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); List createdWidgetsBundles = new ArrayList<>(); List createdSystemWidgetsBundles = new ArrayList<>(); @@ -376,7 +376,7 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { tenant = tenantService.saveTenant(tenant); TenantId tenantId = tenant.getId(); - TenantId systemTenantId = new TenantId(ModelConstants.NULL_UUID); + TenantId systemTenantId = TenantId.fromUUID(ModelConstants.NULL_UUID); List createdWidgetsBundles = new ArrayList<>(); List createdSystemWidgetsBundles = new ArrayList<>(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java index 13b97ea9b1..9d17e5e3e8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/event/BaseEventServiceTest.java @@ -68,7 +68,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); CustomerId customerId = new CustomerId(Uuids.timeBased()); - TenantId tenantId = new TenantId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); @@ -103,7 +103,7 @@ public abstract class BaseEventServiceTest extends AbstractServiceTest { long timeAfterEndTime = LocalDateTime.of(2016, Month.NOVEMBER, 1, 13, 30).toEpochSecond(ZoneOffset.UTC); CustomerId customerId = new CustomerId(Uuids.timeBased()); - TenantId tenantId = new TenantId(Uuids.timeBased()); + TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); saveEventWithProvidedTime(timeBeforeStartTime, customerId, tenantId); Event savedEvent = saveEventWithProvidedTime(eventTime, customerId, tenantId); Event savedEvent2 = saveEventWithProvidedTime(eventTime+1, customerId, tenantId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index d5e743e5af..23ec137dac 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -53,9 +53,9 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); saveAlarm(alarm2Id, tenantId, originator1Id, "TEST_ALARM"); saveAlarm(alarm3Id, tenantId, originator2Id, "TEST_ALARM"); - assertEquals(3, alarmDao.find(new TenantId(tenantId)).size()); + assertEquals(3, alarmDao.find(TenantId.fromUUID(tenantId)).size()); ListenableFuture future = alarmDao - .findLatestByOriginatorAndType(new TenantId(tenantId), new DeviceId(originator1Id), "TEST_ALARM"); + .findLatestByOriginatorAndType(TenantId.fromUUID(tenantId), new DeviceId(originator1Id), "TEST_ALARM"); Alarm alarm = future.get(); assertNotNull(alarm); assertEquals(alarm2Id, alarm.getId().getId()); @@ -64,13 +64,13 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { Alarm alarm = new Alarm(); alarm.setId(new AlarmId(id)); - alarm.setTenantId(new TenantId(tenantId)); + alarm.setTenantId(TenantId.fromUUID(tenantId)); alarm.setOriginator(new DeviceId(deviceId)); alarm.setType(type); alarm.setPropagate(true); alarm.setStartTs(System.currentTimeMillis()); alarm.setEndTs(System.currentTimeMillis()); alarm.setStatus(AlarmStatus.ACTIVE_UNACK); - alarmDao.save(new TenantId(tenantId), alarm); + alarmDao.save(TenantId.fromUUID(tenantId), alarm); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java index a2a8adc927..db563639a6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/asset/JpaAssetDaoTest.java @@ -60,7 +60,7 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { UUID customerId = i % 2 == 0 ? customerId1 : customerId2; saveAsset(assetId, tenantId, customerId, "ASSET_" + i, "TYPE_1"); } - assertEquals(60, assetDao.find(new TenantId(tenantId1)).size()); + assertEquals(60, assetDao.find(TenantId.fromUUID(tenantId1)).size()); PageLink pageLink = new PageLink(20, 0, "ASSET_"); PageData assets1 = assetDao.findAssetsByTenantId(tenantId1, pageLink); @@ -213,10 +213,10 @@ public class JpaAssetDaoTest extends AbstractJpaDaoTest { private void saveAsset(UUID id, UUID tenantId, UUID customerId, String name, String type) { Asset asset = new Asset(); asset.setId(new AssetId(id)); - asset.setTenantId(new TenantId(tenantId)); + asset.setTenantId(TenantId.fromUUID(tenantId)); asset.setCustomerId(new CustomerId(customerId)); asset.setName(name); asset.setType(type); - assetDao.save(new TenantId(tenantId), asset); + assetDao.save(TenantId.fromUUID(tenantId), asset); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java index 9e62116225..e00f6a1dca 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java @@ -76,8 +76,8 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest { private void createCustomer(UUID tenantId, int index) { Customer customer = new Customer(); customer.setId(new CustomerId(Uuids.timeBased())); - customer.setTenantId(new TenantId(tenantId)); + customer.setTenantId(TenantId.fromUUID(tenantId)); customer.setTitle("CUSTOMER_" + index); - customerDao.save(new TenantId(tenantId), customer); + customerDao.save(TenantId.fromUUID(tenantId), customer); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java index 174317ec00..aa23d3dff4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java @@ -60,7 +60,7 @@ public class JpaDashboardInfoDaoTest extends AbstractJpaDaoTest { private void createDashboard(UUID tenantId, int index) { DashboardInfo dashboardInfo = new DashboardInfo(); dashboardInfo.setId(new DashboardId(Uuids.timeBased())); - dashboardInfo.setTenantId(new TenantId(tenantId)); + dashboardInfo.setTenantId(TenantId.fromUUID(tenantId)); dashboardInfo.setTitle("DASHBOARD_" + index); dashboardInfoDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, dashboardInfo); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java index 9d9d128966..8c83a2050b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/device/JpaDeviceDaoTest.java @@ -81,15 +81,15 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { UUID tenantId = Uuids.timeBased(); UUID customerId = Uuids.timeBased(); Device device = getDevice(tenantId, customerId); - deviceDao.save(new TenantId(tenantId), device); + deviceDao.save(TenantId.fromUUID(tenantId), device); UUID uuid = device.getId().getId(); - Device entity = deviceDao.findById(new TenantId(tenantId), uuid); + Device entity = deviceDao.findById(TenantId.fromUUID(tenantId), uuid); assertNotNull(entity); assertEquals(uuid, entity.getId().getId()); executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); - ListenableFuture future = executor.submit(() -> deviceDao.findById(new TenantId(tenantId), uuid)); + ListenableFuture future = executor.submit(() -> deviceDao.findById(TenantId.fromUUID(tenantId), uuid)); Device asyncDevice = future.get(); assertNotNull("Async device expected to be not null", asyncDevice); } @@ -106,8 +106,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { for(int i = 0; i < 5; i++) { UUID deviceId1 = Uuids.timeBased(); UUID deviceId2 = Uuids.timeBased(); - deviceDao.save(new TenantId(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); - deviceDao.save(new TenantId(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); + deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); + deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); deviceIds.add(deviceId1); deviceIds.add(deviceId2); } @@ -129,8 +129,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { for(int i = 0; i < 20; i++) { UUID deviceId1 = Uuids.timeBased(); UUID deviceId2 = Uuids.timeBased(); - deviceDao.save(new TenantId(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); - deviceDao.save(new TenantId(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); + deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1, deviceId1)); + deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2, deviceId2)); deviceIds.add(deviceId1); deviceIds.add(deviceId2); } @@ -142,8 +142,8 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { private void createDevices(UUID tenantId1, UUID tenantId2, UUID customerId1, UUID customerId2, int count) { for (int i = 0; i < count / 2; i++) { - deviceDao.save(new TenantId(tenantId1), getDevice(tenantId1, customerId1)); - deviceDao.save(new TenantId(tenantId2), getDevice(tenantId2, customerId2)); + deviceDao.save(TenantId.fromUUID(tenantId1), getDevice(tenantId1, customerId1)); + deviceDao.save(TenantId.fromUUID(tenantId2), getDevice(tenantId2, customerId2)); } } @@ -154,7 +154,7 @@ public class JpaDeviceDaoTest extends AbstractJpaDaoTest { private Device getDevice(UUID tenantId, UUID customerID, UUID deviceId) { Device device = new Device(); device.setId(new DeviceId(deviceId)); - device.setTenantId(new TenantId(tenantId)); + device.setTenantId(TenantId.fromUUID(tenantId)); device.setCustomerId(new CustomerId(customerID)); device.setName("SEARCH_TEXT"); return device; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java index 57b9519f44..41f0c6985c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java @@ -149,10 +149,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { String type = i % 2 == 0 ? STATS : ALARM; UUID eventId1 = Uuids.timeBased(); Event event1 = getEvent(eventId1, tenantId, entityId1, type); - eventDao.save(new TenantId(tenantId), event1); + eventDao.save(TenantId.fromUUID(tenantId), event1); UUID eventId2 = Uuids.timeBased(); Event event2 = getEvent(eventId2, tenantId, entityId2, type); - eventDao.save(new TenantId(tenantId), event2); + eventDao.save(TenantId.fromUUID(tenantId), event2); } return System.currentTimeMillis(); } @@ -161,10 +161,10 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { for (int i = 0; i < count / 2; i++) { UUID eventId1 = Uuids.timeBased(); Event event1 = getEvent(eventId1, tenantId, entityId1); - eventDao.save(new TenantId(tenantId), event1); + eventDao.save(TenantId.fromUUID(tenantId), event1); UUID eventId2 = Uuids.timeBased(); Event event2 = getEvent(eventId2, tenantId, entityId2); - eventDao.save(new TenantId(tenantId), event2); + eventDao.save(TenantId.fromUUID(tenantId), event2); } return System.currentTimeMillis(); } @@ -178,7 +178,7 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest { private Event getEvent(UUID eventId, UUID tenantId, UUID entityId) { Event event = new Event(); event.setId(new EventId(eventId)); - event.setTenantId(new TenantId(tenantId)); + event.setTenantId(TenantId.fromUUID(tenantId)); EntityId deviceId = new DeviceId(entityId); event.setEntityId(deviceId); event.setUid(event.getId().getId().toString()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java index 3b31be7a04..b965bd2a0c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/tenant/JpaTenantDaoTest.java @@ -69,7 +69,7 @@ public class JpaTenantDaoTest extends AbstractJpaDaoTest { private void createTenant(String region, String title, int index) { Tenant tenant = new Tenant(); - tenant.setId(new TenantId(Uuids.timeBased())); + tenant.setId(TenantId.fromUUID(Uuids.timeBased())); tenant.setRegion(region); tenant.setTitle(title + "_" + index); tenantDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, tenant); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java index b22dd08381..213f9c1d6b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/user/JpaUserDaoTest.java @@ -111,7 +111,7 @@ public class JpaUserDaoTest extends AbstractJpaDaoTest { public void testSave() throws IOException { User user = new User(); user.setId(new UserId(UUID.fromString("cd481534-27cc-11e7-93ae-92361f002671"))); - user.setTenantId(new TenantId(UUID.fromString("1edcb2c6-27cb-11e7-93ae-92361f002671"))); + user.setTenantId(TenantId.fromUUID(UUID.fromString("1edcb2c6-27cb-11e7-93ae-92361f002671"))); user.setCustomerId(new CustomerId(UUID.fromString("51477cb4-27cb-11e7-93ae-92361f002671"))); user.setEmail("user@thingsboard.org"); user.setFirstName("Jackson"); @@ -140,7 +140,7 @@ public class JpaUserDaoTest extends AbstractJpaDaoTest { User user = new User(); UUID id = Uuids.timeBased(); user.setId(new UserId(id)); - user.setTenantId(new TenantId(tenantId)); + user.setTenantId(TenantId.fromUUID(tenantId)); user.setCustomerId(new CustomerId(customerId)); if (customerId == NULL_UUID) { user.setAuthority(Authority.TENANT_ADMIN); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java index 11e2a47af0..54a97ea088 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java @@ -149,7 +149,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest { widgetsBundle.setAlias(prefix + i); widgetsBundle.setTitle(prefix + i); widgetsBundle.setId(new WidgetsBundleId(Uuids.timeBased())); - widgetsBundle.setTenantId(new TenantId(tenantId)); + widgetsBundle.setTenantId(TenantId.fromUUID(tenantId)); widgetsBundleDao.save(AbstractServiceTest.SYSTEM_TENANT_ID, widgetsBundle); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java index 28eafabfce..05b3bdc1d0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java @@ -208,7 +208,7 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { - return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), new TenantId(entityContainer.getEntityId().getId())), tenant -> { + return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), TenantId.fromUUID(entityContainer.getEntityId().getId())), tenant -> { if (tenant != null) { return processSave(ctx, sdId, relationType); } else { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 0b800eb22c..e2de0bb55f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -98,7 +98,7 @@ public class TbAlarmNodeTest { private EntityId originator = new DeviceId(Uuids.timeBased()); private EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); - private TenantId tenantId = new TenantId(Uuids.timeBased()); + private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); private TbMsgMetaData metaData = new TbMsgMetaData(); private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java index a0f2879851..b8265e2a2e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java @@ -68,7 +68,7 @@ import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @RunWith(MockitoJUnitRunner.class) public abstract class AbstractAttributeNodeTest { final CustomerId customerId = new CustomerId(Uuids.timeBased()); - final TenantId tenantId = new TenantId(Uuids.timeBased()); + final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); final String keyAttrConf = "${word}"; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index b2c79102c4..0f88f9baac 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -99,7 +99,7 @@ public class TbDeviceProfileNodeTest { @Mock private AttributesService attributesService; - private final TenantId tenantId = new TenantId(UUID.randomUUID()); + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); private final DeviceId deviceId = new DeviceId(UUID.randomUUID()); private final CustomerId customerId = new CustomerId(UUID.randomUUID()); private final DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); From 8611349e4b8c08add2206777475d4e2808088891 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 15:03:15 +0200 Subject: [PATCH 063/403] TenantId static initializer --- .../org/thingsboard/server/common/data/id/TenantId.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index 665edc0ca9..77a3480f0a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -28,10 +28,14 @@ import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore - public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); + public static final TenantId SYS_TENANT_ID = new TenantId(EntityId.NULL_UUID); static final ConcurrentReferenceHashMap tenants = new ConcurrentReferenceHashMap<>(16, ReferenceType.SOFT); + static { + tenants.put(SYS_TENANT_ID.getId(), SYS_TENANT_ID); + } + private static final long serialVersionUID = 1L; @JsonCreator From fa1cd21473e5fe6e2c21758b95846ae39685ea18 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 10 Jan 2022 15:21:44 +0200 Subject: [PATCH 064/403] UI: Fixed cancel event on color picker --- .../shared/components/dialog/color-picker-dialog.component.ts | 2 +- .../src/app/shared/components/json-form/json-form.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 2374749cbc..499f857df8 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -67,7 +67,7 @@ export class ColorPickerDialogComponent extends DialogComponent void) { this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { - if (colorSelectedFn) { + if (color && colorSelectedFn) { colorSelectedFn(tinycolor(color).toRgb()); } }); From 2e9d7768450c8da689e221cd298364e1f0d5f5cf Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 17:16:30 +0200 Subject: [PATCH 065/403] TenantId static initializer replaced with a static variable order --- .../org/thingsboard/server/common/data/id/TenantId.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index 77a3480f0a..1d62d8e3f2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -28,13 +28,10 @@ import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore - public static final TenantId SYS_TENANT_ID = new TenantId(EntityId.NULL_UUID); - static final ConcurrentReferenceHashMap tenants = new ConcurrentReferenceHashMap<>(16, ReferenceType.SOFT); - static { - tenants.put(SYS_TENANT_ID.getId(), SYS_TENANT_ID); - } + @JsonIgnore + public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); private static final long serialVersionUID = 1L; From 0ef9d5753cd271fd7c8729567b5a2765943538d1 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 17:36:57 +0200 Subject: [PATCH 066/403] MqttTransportHandler refactored to not reference on InetSocketAddress instances to free some heap space. IPv4 are stored and logged as int. --- .../transport/mqtt/MqttTransportHandler.java | 25 +++++++++++++------ .../mqtt/session/DeviceSessionCtx.java | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index bf474cc266..fe39721f27 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -76,6 +76,7 @@ import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; import javax.net.ssl.SSLPeerUnverifiedException; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.ByteBuffer; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -111,7 +112,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN); private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN); - private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"; private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; @@ -124,7 +124,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final ConcurrentMap mqttQoSMap; final DeviceSessionCtx deviceSessionCtx; - volatile InetSocketAddress address; + volatile int ip = 0; + volatile int port = 0; volatile GatewaySessionHandler gatewaySessionHandler; private final ConcurrentHashMap otaPackSessions; @@ -183,9 +184,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { - address = getAddress(ctx); + if (port == 0) { + InetSocketAddress address = getAddress(ctx); + ip = getIpv4(address); //ipv6 will not appear in logs + port = address.getPort(); + } + if (msg.fixedHeader() == null) { - log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); + log.info("[{}:{}] Invalid message received", ip, port); ctx.close(); return; } @@ -199,6 +205,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + int getIpv4(InetSocketAddress address) { + byte[] ipBytes = address.getAddress().getAddress(); + return ipBytes.length == 4 ? ByteBuffer.wrap(ipBytes).getInt() : -1; + } + InetSocketAddress getAddress(ChannelHandlerContext ctx) { return (InetSocketAddress) ctx.channel().remoteAddress(); } @@ -791,7 +802,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, userName, e); + log.trace("[{}] Failed to process credentials: {}", ip, userName, e); ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage)); ctx.close(); } @@ -814,14 +825,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); + log.trace("[{}] Failed to process credentials: {}", ip, sha3Hash, e); ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage)); ctx.close(); } }); } catch (Exception e) { ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED, connectMessage)); - log.trace("[{}] X509 auth failure: {}", sessionId, address, e); + log.trace("[{}] X509 auth failure: {}", sessionId, ip, e); ctx.close(); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index 9f0c89714a..76e2911793 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -238,7 +238,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public void release() { if (!msgQueue.isEmpty()) { - log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", getDeviceId(), msgQueue.size()); + log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue. cleared", getDeviceId(), getMsgQueueSize()); msgQueue.forEach(ReferenceCountUtil::safeRelease); msgQueue.clear(); } From ef53df75128a0502c5727d4229acd37cd79c1aaa Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 18:41:53 +0200 Subject: [PATCH 067/403] MqttTransportHandler fixed test --- .../server/transport/mqtt/MqttTransportHandlerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java index a9903fe867..7701a82cbe 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportHandlerTest.java @@ -23,7 +23,6 @@ import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectPayload; import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; import io.netty.handler.codec.mqtt.MqttFixedHeader; -import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; @@ -118,7 +117,8 @@ public class MqttTransportHandlerTest { handler.processMqttMsg(ctx, msg); - assertThat(handler.address, is(IP_ADDR)); + assertThat(handler.ip, is(handler.getIpv4(IP_ADDR))); + assertThat(handler.port, is(IP_ADDR.getPort())); assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); verify(handler, never()).doDisconnect(); verify(handler, times(1)).processConnect(ctx, msg); @@ -154,7 +154,7 @@ public class MqttTransportHandlerTest { messages.forEach((msg) -> handler.processMqttMsg(ctx, msg)); - assertThat(handler.address, is(IP_ADDR)); + assertThat(handler.ip, is(handler.getIpv4(IP_ADDR))); assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); assertThat(handler.deviceSessionCtx.isConnected(), is(false)); assertThat(handler.deviceSessionCtx.getMsgQueueSize(), is(MSG_QUEUE_LIMIT)); From c4d180eecc3f147bbf35735074b0ce957b31d8f3 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 19:25:56 +0200 Subject: [PATCH 068/403] DeviceActorMessageProcessor(AbstractContextAwareMsgProcessor): ObjectMapper live instance count reduced with a static ObjectMapper --- .../actors/shared/AbstractContextAwareMsgProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java index a5de546cb6..0a89cabe5d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java @@ -22,13 +22,13 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.common.msg.TbActorMsg; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; @Slf4j public abstract class AbstractContextAwareMsgProcessor { + protected final static ObjectMapper mapper = new ObjectMapper(); + protected final ActorSystemContext systemContext; - protected final ObjectMapper mapper = new ObjectMapper(); protected AbstractContextAwareMsgProcessor(ActorSystemContext systemContext) { super(); From f5c09d221de6ce6f307edc59e3ee0db39a382d3e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 23:47:50 +0200 Subject: [PATCH 069/403] TbMsgMetaData refactored constructor and copy method --- .../server/common/msg/TbMsgMetaData.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index 1fa6f0d4f4..8f23a917a2 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.common.msg; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Collections; @@ -29,15 +27,18 @@ import java.util.concurrent.ConcurrentHashMap; * Created by ashvayka on 13.01.18. */ @Data -@NoArgsConstructor public final class TbMsgMetaData implements Serializable { public static final TbMsgMetaData EMPTY = new TbMsgMetaData(Collections.emptyMap()); - private final Map data = new ConcurrentHashMap<>(); + private final Map data; + + public TbMsgMetaData() { + this.data = new ConcurrentHashMap<>(); + } public TbMsgMetaData(Map data) { - data.forEach((key, val) -> putValue(key, val)); + this.data = new ConcurrentHashMap<>(data); } public String getValue(String key) { @@ -55,6 +56,6 @@ public final class TbMsgMetaData implements Serializable { } public TbMsgMetaData copy() { - return new TbMsgMetaData(new ConcurrentHashMap<>(data)); + return new TbMsgMetaData(data); } } From a11146b44592d13ef636e397a24250ebe4e910cb Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 10 Jan 2022 23:55:58 +0200 Subject: [PATCH 070/403] immutable TbMsgMetaData.EMPTY --- .../org/thingsboard/server/common/msg/TbMsgMetaData.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index 8f23a917a2..2376a7a35f 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -29,7 +29,7 @@ import java.util.concurrent.ConcurrentHashMap; @Data public final class TbMsgMetaData implements Serializable { - public static final TbMsgMetaData EMPTY = new TbMsgMetaData(Collections.emptyMap()); + public static final TbMsgMetaData EMPTY = new TbMsgMetaData(0); private final Map data; @@ -41,6 +41,13 @@ public final class TbMsgMetaData implements Serializable { this.data = new ConcurrentHashMap<>(data); } + /** + * Internal constructor to create immutable TbMsgMetaData.EMPTY + * */ + private TbMsgMetaData(int ignored) { + this.data = Collections.emptyMap(); + } + public String getValue(String key) { return data.get(key); } From d46575aeafddda85cf37b4de9b36103e660d5607 Mon Sep 17 00:00:00 2001 From: Nikitozin Date: Tue, 11 Jan 2022 11:49:28 +0200 Subject: [PATCH 071/403] [WIP][3.3.3] Add statement for support double type. --- .../engine/action/TbSaveToCustomCassandraTableNode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index a56cb644be..bd3371edae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -182,7 +182,11 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (dataAsObject.get(key).isJsonPrimitive()) { JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); if (primitive.isNumber()) { - stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + if (dataAsObject.get(key).getAsString().contains(".")) { + stmtBuilder.setDouble(i.get(), dataAsObject.get(key).getAsDouble()); + } else { + stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + } } else if (primitive.isBoolean()) { stmtBuilder.setBoolean(i.get(), dataAsObject.get(key).getAsBoolean()); } else if (primitive.isString()) { From b0a79e9fc3236dffd2a763274cb7c9d15cc797fa Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 11 Jan 2022 12:23:48 +0200 Subject: [PATCH 072/403] static transport immutable proto SESSION_EVENT_MSG_OPEN, SESSION_EVENT_MSG_CLOSED, SESSION_CLOSE_NOTIFICATION_PROTO, SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG, SUBSCRIBE_TO_RPC_ASYNC_MSG --- .../session/DefaultLwM2MSessionManager.java | 14 +++++++---- .../transport/mqtt/MqttTransportHandler.java | 7 +++--- .../mqtt/session/GatewaySessionHandler.java | 15 ++++++------ .../service/DefaultTransportService.java | 24 ++++++++++--------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/session/DefaultLwM2MSessionManager.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/session/DefaultLwM2MSessionManager.java index ee6e4a779b..a9198f9fcd 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/session/DefaultLwM2MSessionManager.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/session/DefaultLwM2MSessionManager.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener; @@ -27,6 +26,11 @@ import org.thingsboard.server.transport.lwm2m.server.attributes.LwM2MAttributesS import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcRequestHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; + @Slf4j @Service @TbLwM2mTransportComponent @@ -52,16 +56,16 @@ public class DefaultLwM2MSessionManager implements LwM2MSessionManager { transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(uplinkHandler, attributesService, rpcHandler, sessionInfo, transportService)); TransportProtos.TransportToDeviceActorMsg msg = TransportProtos.TransportToDeviceActorMsg.newBuilder() .setSessionInfo(sessionInfo) - .setSessionEvent(DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.OPEN)) - .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setSessionType(TransportProtos.SessionType.ASYNC).build()) - .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder().setSessionType(TransportProtos.SessionType.ASYNC).build()) + .setSessionEvent(SESSION_EVENT_MSG_OPEN) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) .build(); transportService.process(msg, null); } @Override public void deregister(TransportProtos.SessionInfoProto sessionInfo) { - transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null); + transportService.process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); transportService.deregisterSession(sessionInfo); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index fe39721f27..c99b4e1a45 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -65,7 +65,6 @@ import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; @@ -102,6 +101,8 @@ import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; /** * @author Andrew Shvayka @@ -929,7 +930,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void doDisconnect() { if (deviceSessionCtx.isConnected()) { log.debug("[{}] Client disconnected!", sessionId); - transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); if (gatewaySessionHandler != null) { gatewaySessionHandler.onGatewayDisconnect(); @@ -948,7 +949,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); deviceSessionCtx.setDeviceProfile(msg.getDeviceProfile()); deviceSessionCtx.setSessionInfo(SessionInfoCreator.create(msg, context, sessionId)); - transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_OPEN, new TransportServiceCallback() { @Override public void onSuccess(Void msg) { SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 7b45b5f9d9..cb984327da 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; -import org.thingsboard.server.common.transport.service.DefaultTransportService; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; @@ -68,6 +67,10 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; /** * Created by ashvayka on 19.01.17. @@ -267,11 +270,9 @@ public class GatewaySessionHandler { transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() .setSessionInfo(deviceSessionInfo) - .setSessionEvent(DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.OPEN)) - .setSubscribeToAttributes(TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder() - .setSessionType(TransportProtos.SessionType.ASYNC).build()) - .setSubscribeToRPC(TransportProtos.SubscribeToRPCMsg.newBuilder() - .setSessionType(TransportProtos.SessionType.ASYNC).build()) + .setSessionEvent(SESSION_EVENT_MSG_OPEN) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) .build(), null); } futureToSet.set(devices.get(deviceName)); @@ -720,7 +721,7 @@ public class GatewaySessionHandler { private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); - transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null); + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 57d1da63b4..98ffcd721e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.protobuf.ByteString; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -124,6 +123,15 @@ import java.util.stream.Collectors; public class DefaultTransportService implements TransportService { public static final String OVERWRITE_ACTIVITY_TIME = "overwriteActivityTime"; + public static final String SESSION_EXPIRED_MESSAGE = "Session has expired due to last activity time!"; + public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_OPEN = getSessionEventMsg(TransportProtos.SessionEvent.OPEN); + public static final TransportProtos.SessionEventMsg SESSION_EVENT_MSG_CLOSED = getSessionEventMsg(TransportProtos.SessionEvent.CLOSED); + public static final TransportProtos.SessionCloseNotificationProto SESSION_CLOSE_NOTIFICATION_PROTO = TransportProtos.SessionCloseNotificationProto.newBuilder() + .setMessage(SESSION_EXPIRED_MESSAGE).build(); + public static final TransportProtos.SubscribeToAttributeUpdatesMsg SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG = TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC).build(); + public static final TransportProtos.SubscribeToRPCMsg SUBSCRIBE_TO_RPC_ASYNC_MSG = TransportProtos.SubscribeToRPCMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC).build(); private final AtomicInteger atomicTs = new AtomicInteger(0); @@ -267,9 +275,7 @@ public class DefaultTransportService implements TransportService { @Override public SessionMetaData registerAsyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener) { - SessionMetaData newValue = new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener); - SessionMetaData oldValue = sessions.putIfAbsent(toSessionId(sessionInfo), newValue); - return oldValue != null ? oldValue : newValue; + return sessions.computeIfAbsent(toSessionId(sessionInfo), (x) -> new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener)); } @Override @@ -719,12 +725,8 @@ public class DefaultTransportService implements TransportService { } sessions.remove(uuid); sessionsToRemove.add(uuid); - process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.CLOSED), null); - TransportProtos.SessionCloseNotificationProto sessionCloseNotificationProto = TransportProtos.SessionCloseNotificationProto - .newBuilder() - .setMessage("Session has expired due to last activity time!") - .build(); - sessionMD.getListener().onRemoteSessionCloseCommand(uuid, sessionCloseNotificationProto); + process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null); + sessionMD.getListener().onRemoteSessionCloseCommand(uuid, SESSION_CLOSE_NOTIFICATION_PROTO); } } else { if (lastActivityTime > sessionAD.getLastReportedActivityTime()) { @@ -1018,7 +1020,7 @@ public class DefaultTransportService implements TransportService { return new DeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); } - public static TransportProtos.SessionEventMsg getSessionEventMsg(TransportProtos.SessionEvent event) { + private static TransportProtos.SessionEventMsg getSessionEventMsg(TransportProtos.SessionEvent event) { return TransportProtos.SessionEventMsg.newBuilder() .setSessionType(TransportProtos.SessionType.ASYNC) .setEvent(event).build(); From 3c7ec8b7d3de98e20753aa4d31f5e3ba913b3c09 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 12:45:34 +0200 Subject: [PATCH 073/403] lwm2m tests refactoring by comments --- .../thingsboard/server/controller/TbTestWebSocketClient.java | 2 +- .../server/transport/lwm2m/AbstractLwM2MIntegrationTest.java | 1 - .../server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 2bb68737ac..ff6b004405 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -74,7 +74,7 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TimeUnit.SECONDS.toMillis(8)); + return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); } public String waitForUpdate(long ms) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 09495402f3..4fc9108862 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m; import com.fasterxml.jackson.core.type.TypeReference; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.leshan.client.object.Security; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index e3c538c928..3b152ec728 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -200,8 +200,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { * */ @Test public void testSoftwareUpdateByObject9() throws Exception { -// String endpoint = "Ota9_Device"; -// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9); final Device device = createDevice(credentials); From 217a84f1f2ea3bbed90ba0e2d624ed4e4cd94665 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 11 Jan 2022 12:56:02 +0200 Subject: [PATCH 074/403] request added to the log on "Pending request map is full" in DefaultTbQueueRequestTemplate.java --- .../server/queue/common/DefaultTbQueueRequestTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java index 7463c11df5..58890fb2a3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java @@ -210,7 +210,7 @@ public class DefaultTbQueueRequestTemplate send(Request request, long requestTimeoutNs) { if (pendingRequests.mappingCount() >= maxPendingRequests) { - log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance", maxPendingRequests); + log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance. Request is {}", maxPendingRequests, request); return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!")); } UUID requestId = UUID.randomUUID(); From 6479160932d8a6f78e9e556a5349ef1c8946e8e9 Mon Sep 17 00:00:00 2001 From: Nikitozin Date: Tue, 11 Jan 2022 13:44:47 +0200 Subject: [PATCH 075/403] [WIP][3.3.3] Refactor variables. --- .../action/TbSaveToCustomCassandraTableNode.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index bd3371edae..66291f5245 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -179,18 +179,19 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (key.equals(ENTITY_ID)) { stmtBuilder.setUuid(i.get(), msg.getOriginator().getId()); } else if (dataAsObject.has(key)) { - if (dataAsObject.get(key).isJsonPrimitive()) { - JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); + JsonElement dataKeyElement = dataAsObject.get(key); + if (dataKeyElement.isJsonPrimitive()) { + JsonPrimitive primitive = dataKeyElement.getAsJsonPrimitive(); if (primitive.isNumber()) { - if (dataAsObject.get(key).getAsString().contains(".")) { - stmtBuilder.setDouble(i.get(), dataAsObject.get(key).getAsDouble()); + if (primitive.getAsString().contains(".")) { + stmtBuilder.setDouble(i.get(), primitive.getAsDouble()); } else { - stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + stmtBuilder.setLong(i.get(), primitive.getAsLong()); } } else if (primitive.isBoolean()) { - stmtBuilder.setBoolean(i.get(), dataAsObject.get(key).getAsBoolean()); + stmtBuilder.setBoolean(i.get(), primitive.getAsBoolean()); } else if (primitive.isString()) { - stmtBuilder.setString(i.get(), dataAsObject.get(key).getAsString()); + stmtBuilder.setString(i.get(), primitive.getAsString()); } else { stmtBuilder.setToNull(i.get()); } From 667045384649ac02f86e73418cf3daf07259c323 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 11 Jan 2022 14:59:49 +0200 Subject: [PATCH 076/403] UI: Added persistent page link for entities pages --- .../components/alarm/alarm-table-config.ts | 1 + .../audit-log/audit-log-table-config.ts | 4 +- .../audit-log/audit-log-table.component.ts | 8 +- .../entity/entities-table.component.ts | 96 ++++++++++++++----- .../components/event/event-table-config.ts | 1 + .../entity/entities-table-config.models.ts | 1 + .../entity/entity-table-component.models.ts | 1 + .../audit-log/audit-log-routing.module.ts | 3 +- 8 files changed, 88 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 2bff69a53e..6c4e0a22e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -60,6 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; + this.persistentPageLinkMode = false; this.defaultTimewindowInterval = historyInterval(DAY * 30); this.detailsPanelEnabled = false; this.selectionEnabled = false; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index f5c928f275..a61d0c54ab 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -52,11 +52,13 @@ export class AuditLogTableConfig extends EntityTableConfig) { + private store: Store, + private route: ActivatedRoute) { } ngOnInit() { @@ -117,6 +119,7 @@ export class AuditLogTableComponent implements OnInit { } updateOnInit = true; } + const persistentPageLinkMode = !!this.route.snapshot.data.isPage; this.auditLogTableConfig = new AuditLogTableConfig( this.auditLogService, this.translate, @@ -126,7 +129,8 @@ export class AuditLogTableComponent implements OnInit { this.entityIdValue, this.userIdValue, this.customerIdValue, - updateOnInit + updateOnInit, + persistentPageLinkMode ); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 33f1b2afe4..8fd2f8a976 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -41,9 +41,10 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; import { - CellActionDescriptor, CellActionDescriptorType, + CellActionDescriptor, + CellActionDescriptorType, EntityActionTableColumn, EntityColumn, EntityTableColumn, @@ -55,14 +56,10 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { - calculateIntervalStartEndTime, - HistoryWindowType, - Timewindow -} from '@shared/models/time/time.models'; +import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; -import { isDefined, isUndefined } from '@core/utils'; +import { isDefined, isEmptyStr, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; @@ -101,6 +98,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa displayPagination = true; pageSizeOptions; pageLink: PageLink; + persistentPageLinkMode = true; textSearchMode = false; timewindow: Timewindow; dataSource: EntitiesDataSource>; @@ -117,7 +115,6 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - private sortSubscription: Subscription; private updateDataSubscription: Subscription; private viewInited = false; @@ -128,6 +125,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, + private router: Router, private componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -196,16 +194,19 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.columnsUpdated(); + const routerQueryParams = this.route.snapshot.queryParams; + let sortOrder: SortOrder = null; - if (this.entitiesTableConfig.defaultSortOrder) { + if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) { sortOrder = { - property: this.entitiesTableConfig.defaultSortOrder.property, - direction: this.entitiesTableConfig.defaultSortOrder.direction + property: routerQueryParams?.property || this.entitiesTableConfig.defaultSortOrder.property, + direction: routerQueryParams?.direction || this.entitiesTableConfig.defaultSortOrder.direction }; } this.displayPagination = this.entitiesTableConfig.displayPagination; this.defaultPageSize = this.entitiesTableConfig.defaultPageSize; + this.persistentPageLinkMode = this.entitiesTableConfig.persistentPageLinkMode; this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; if (this.entitiesTableConfig.useTimePageLink) { @@ -217,6 +218,16 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.pageLink = new PageLink(10, 0, null, sortOrder); } this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; + if (routerQueryParams.hasOwnProperty('page')) { + this.pageLink.page = routerQueryParams.page; + } + if (routerQueryParams.hasOwnProperty('pageSize')) { + this.pageLink.pageSize = routerQueryParams.pageSize; + } + if (routerQueryParams.hasOwnProperty('textSearch') && !isEmptyStr(routerQueryParams.textSearch)) { + this.textSearchMode = true; + this.pageLink.textSearch = decodeURI(routerQueryParams.textSearch); + } this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this)); if (this.entitiesTableConfig.onLoadAction) { this.entitiesTableConfig.onLoadAction(this.route); @@ -238,9 +249,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa debounceTime(150), distinctUntilChanged(), tap(() => { + const queryParams: any = { + textSearch: encodeURI(this.pageLink.textSearch) || null + }; if (this.displayPagination) { this.paginator.pageIndex = 0; + queryParams.page = null; } + this.updatedRouterQueryParams(queryParams); this.updateData(); }) ) @@ -251,23 +267,42 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } private updatePaginationSubscriptions() { - if (this.sortSubscription) { - this.sortSubscription.unsubscribe(); - this.sortSubscription = null; - } if (this.updateDataSubscription) { this.updateDataSubscription.unsubscribe(); this.updateDataSubscription = null; } + let paginatorSubscription$: Observable; + const sortSubscription$: Observable = this.sort.sortChange.asObservable().pipe( + map((data) => { + const direction = data.direction.toUpperCase(); + const queryParams: any = { + direction: this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction, + property: this.entitiesTableConfig?.defaultSortOrder?.property === data.active ? null : data.active + }; + if (this.displayPagination) { + queryParams.page = null; + this.paginator.pageIndex = 0; + } + return queryParams; + }) + ); if (this.displayPagination) { - this.sortSubscription = this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + paginatorSubscription$ = this.paginator.page.asObservable().pipe( + map((data) => { + return { + page: data.pageIndex === 0 ? null : data.pageIndex, + pageSize: data.pageSize === this.defaultPageSize ? null : data.pageSize + }; + }) + ); } - this.updateDataSubscription = ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) - : this.sort.sortChange) as Observable) - .pipe( - tap(() => this.updateData()) - ) - .subscribe(); + this.updateDataSubscription = ((this.displayPagination ? merge(sortSubscription$, paginatorSubscription$) + : sortSubscription$) as Observable).pipe( + tap((queryParams) => { + this.updatedRouterQueryParams(queryParams); + this.updateData(); + }) + ).subscribe(); } addEnabled() { @@ -451,9 +486,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa exitFilterMode() { this.textSearchMode = false; this.pageLink.textSearch = null; + const queryParams: any = { + textSearch: null + }; if (this.displayPagination) { this.paginator.pageIndex = 0; + queryParams.page = null; } + this.updatedRouterQueryParams(queryParams); this.updateData(); } @@ -468,6 +508,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); this.sort.active = sortable.id; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; + this.updatedRouterQueryParams({}, 'preserve'); if (update) { this.updateData(); } @@ -587,4 +628,13 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return entity.id.id; } + protected updatedRouterQueryParams(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { + if (this.persistentPageLinkMode) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling + }); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index d407cf262b..d6eedb7ff6 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -92,6 +92,7 @@ export class EventTableConfig extends EntityTableConfig { this.searchEnabled = false; this.addEnabled = false; this.entitiesDeleteEnabled = false; + this.persistentPageLinkMode = false; this.headerComponent = EventTableHeaderComponent; diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 93e68e40b7..d6ddf40735 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -160,6 +160,7 @@ export class EntityTableConfig, P extends PageLink = P addDialogStyle = {}; defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC}; displayPagination = true; + persistentPageLinkMode = true; defaultPageSize = 10; columns: Array> = []; cellActionDescriptors: Array> = []; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 20b1d15b32..4cf41f0fed 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -50,6 +50,7 @@ export interface IEntitiesTableComponent { displayPagination: boolean; pageSizeOptions: number[]; pageLink: PageLink; + persistentPageLinkMode: boolean; textSearchMode: boolean; timewindow: Timewindow; dataSource: EntitiesDataSource>; diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts index 89d304e8f1..b471f3b540 100644 --- a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts @@ -29,7 +29,8 @@ const routes: Routes = [ breadcrumb: { label: 'audit-log.audit-logs', icon: 'track_changes' - } + }, + isPage: true } } ]; From 9de9e6147fa5ef6db5d644b3cffd36bd6422a718 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 17:46:06 +0200 Subject: [PATCH 077/403] lwm2m fix bug update profile if security mode == null --- .../3.3.2/schema_update_lwm2m_bootstrap.sql | 51 ++++++++++++------- .../install/SqlDatabaseUpgradeService.java | 2 + 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql index a182bc8117..8daa1b1e1e 100644 --- a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql +++ b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql @@ -14,6 +14,7 @@ -- limitations under the License. -- + CREATE OR REPLACE PROCEDURE update_profile_bootstrap() LANGUAGE plpgsql AS $$ @@ -25,9 +26,11 @@ BEGIN profile_data, '{transportConfiguration}', get_bootstrap( - profile_data::jsonb #> '{transportConfiguration}', - subquery.publickey_bs, - subquery.publickey_lw), + profile_data::jsonb #> '{transportConfiguration}', + subquery.publickey_bs, + subquery.publickey_lw, + profile_data::json #>> '{transportConfiguration, bootstrap, bootstrapServer, securityMode}', + profile_data::json #>> '{transportConfiguration, bootstrap, lwm2mServer, securityMode}'), true) FROM ( SELECT id, @@ -48,7 +51,8 @@ END; $$; CREATE OR REPLACE FUNCTION get_bootstrap(transport_configuration_in jsonb, publickey_bs text, - publickey_lw text) RETURNS jsonb AS + publickey_lw text, security_mode_bs text, + security_mode_lw text) RETURNS jsonb AS $$ DECLARE @@ -56,10 +60,19 @@ DECLARE bootstrap_in jsonb; BEGIN + + IF security_mode_lw IS NULL THEN + security_mode_lw := 'NO_SEC'; + END IF; + + IF security_mode_bs IS NULL THEN + security_mode_bs := 'NO_SEC'; + END IF; + bootstrap_in := transport_configuration_in::jsonb #> '{bootstrap}'; bootstrap_new := json_build_array( json_build_object('shortServerId', bootstrap_in::json #> '{bootstrapServer}' -> 'serverId', - 'securityMode', bootstrap_in::json #> '{bootstrapServer}' ->> 'securityMode', + 'securityMode', security_mode_bs, 'binding', bootstrap_in::json #> '{servers}' ->> 'binding', 'lifetime', bootstrap_in::json #> '{servers}' -> 'lifetime', 'notifIfDisabled', bootstrap_in::json #> '{servers}' -> 'notifIfDisabled', @@ -73,7 +86,7 @@ BEGIN bootstrap_in::json #> '{bootstrapServer}' -> 'bootstrapServerAccountTimeout' ), json_build_object('shortServerId', bootstrap_in::json #> '{lwm2mServer}' -> 'serverId', - 'securityMode', bootstrap_in::json #> '{lwm2mServer}' ->> 'securityMode', + 'securityMode', security_mode_lw, 'binding', bootstrap_in::json #> '{servers}' ->> 'binding', 'lifetime', bootstrap_in::json #> '{servers}' -> 'lifetime', 'notifIfDisabled', bootstrap_in::json #> '{servers}' -> 'notifIfDisabled', @@ -93,7 +106,7 @@ BEGIN bootstrap_new, true) || '{"bootstrapServerUpdateEnable": true}'; -END ; +END; $$ LANGUAGE plpgsql; CREATE OR REPLACE PROCEDURE update_device_credentials_to_base64_and_bootstrap() @@ -102,9 +115,9 @@ $$ BEGIN -UPDATE device_credentials -SET credentials_value = get_device_and_bootstrap(credentials_value::text) -WHERE credentials_type = 'LWM2M_CREDENTIALS'; + UPDATE device_credentials + SET credentials_value = get_device_and_bootstrap(credentials_value::text) + WHERE credentials_type = 'LWM2M_CREDENTIALS'; END; $$; @@ -112,7 +125,7 @@ CREATE OR REPLACE FUNCTION get_device_and_bootstrap(IN credentials_value text, O LANGUAGE plpgsql AS $$ DECLARE -client_secret_key text; + client_secret_key text; client_public_key_or_id text; client_key_value_object jsonb; client_bootstrap_server_value_object jsonb; @@ -130,7 +143,7 @@ BEGIN 'key', client_public_key_or_id); credentials_value_new := credentials_value_new::jsonb || json_build_object('client', client_key_value_object)::jsonb; -END IF; + END IF; IF credentials_value::jsonb #> '{client}' ->> 'securityConfigClientMode' = 'X509' AND NULLIF((credentials_value::jsonb #> '{client}' ->> 'cert' ~ '^[0-9a-fA-F]+$')::text, 'false') = 'true' THEN client_public_key_or_id := @@ -141,8 +154,8 @@ END IF; 'cert', client_public_key_or_id); credentials_value_new := credentials_value_new::jsonb || json_build_object('client', client_key_value_object)::jsonb; -END IF; - + END IF; + IF credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'securityMode' = 'RPK' OR credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'securityMode' = 'X509' THEN IF NULLIF((credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'clientSecretKey' ~ '^[0-9a-fA-F]+$')::text, @@ -165,9 +178,9 @@ END IF; client_bootstrap_object := credentials_value_new::jsonb #> '{bootstrap}' || client_bootstrap_server_object::jsonb; credentials_value_new := jsonb_set(credentials_value_new::jsonb, '{bootstrap}', client_bootstrap_object::jsonb, false)::jsonb; -END IF; -END IF; - + END IF; + END IF; + IF credentials_value::jsonb #> '{bootstrap,bootstrapServer}' ->> 'securityMode' = 'RPK' OR credentials_value::jsonb #> '{bootstrap,bootstrapServer}' ->> 'securityMode' = 'X509' THEN IF NULLIF( @@ -193,8 +206,8 @@ END IF; client_bootstrap_object := credentials_value_new::jsonb #> '{bootstrap}' || client_bootstrap_server_object::jsonb; credentials_value_new := jsonb_set(credentials_value_new::jsonb, '{bootstrap}', client_bootstrap_object::jsonb, false)::jsonb; -END IF; -END IF; + END IF; + END IF; END; $$; \ No newline at end of file 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 eef29026ba..c98e46d86e 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 @@ -476,6 +476,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); + loadSql(schemaUpdateFile, conn); conn.createStatement().execute("call update_profile_bootstrap();"); log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); From b49c32e7b15117df4842386eebb02b6e0bedffd2 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 18:19:36 +0200 Subject: [PATCH 078/403] lwm2m frefactoring by comments2 --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 4 +- .../transport/lwm2m/Lwm2mTestHelper.java | 42 +++++----- .../lwm2m/client/LwM2MTestClient.java | 25 ++---- .../client/LwM2mBinaryAppDataContainer.java | 11 +-- .../transport/lwm2m/client/LwM2mLocation.java | 4 - .../ota/AbstractOtaLwM2MIntegrationTest.java | 4 +- .../ota/sql/OtaLwM2MIntegrationTest.java | 2 +- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 54 ++++++------- .../sql/RpcLwm2mIntegrationCreateTest.java | 30 ++++---- .../sql/RpcLwm2mIntegrationDeleteTest.java | 13 ++-- .../sql/RpcLwm2mIntegrationDiscoverTest.java | 8 +- .../sql/RpcLwm2mIntegrationExecuteTest.java | 30 ++++---- .../sql/RpcLwm2mIntegrationObserveTest.java | 26 +++---- .../rpc/sql/RpcLwm2mIntegrationReadTest.java | 68 ++++++++-------- ...pcLwm2mIntegrationWriteAttributesTest.java | 11 +-- .../rpc/sql/RpcLwm2mIntegrationWriteTest.java | 77 +++++++++---------- .../AbstractSecurityLwM2MIntegrationTest.java | 4 +- 17 files changed, 190 insertions(+), 223 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 4fc9108862..402e95b9ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -69,7 +69,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { - protected String transportConfiguration = "{\n" + + protected final String TRANSPORT_CONFIGURATION = "{\n" + " \"type\": \"LWM2M\",\n" + " \"observeAttr\": {\n" + " \"keyName\": {\n" + @@ -176,7 +176,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest LwM2MClientCredential credentials, NetworkConfig coapConfig, String endpoint) throws Exception { - createDeviceProfile(transportConfiguration); + createDeviceProfile(TRANSPORT_CONFIGURATION); Device device = createDevice(credentials); SingleEntityFilter sef = new SingleEntityFilter(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 8dd44d25ae..19bde32467 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -43,26 +43,26 @@ public class Lwm2mTestHelper { public static final int TEMPERATURE_SENSOR = 3303; // Ids in Client - public static final int objectId_0 = 0; - public static final int objectInstanceId_0 = 0; - public static final int objectInstanceId_1 = 1; - public static final int objectInstanceId_2 = 2; - public static final int objectInstanceId_12 = 12; - public static final int resourceId_0 = 0; - public static final int resourceId_1 = 1; - public static final int resourceId_2 = 2; - public static final int resourceId_3 = 3; - public static final int resourceId_4 = 4; - public static final int resourceId_7 = 7; - public static final int resourceId_8 = 8; - public static final int resourceId_9 = 9; - public static final int resourceId_11 = 11; - public static final int resourceId_14 = 14; - public static final int resourceId_15= 15; - public static final int resourceInstanceId_2 = 2; + public static final int OBJECT_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_1 = 1; + public static final int OBJECT_INSTANCE_ID_2 = 2; + public static final int OBJECT_INSTANCE_ID_12 = 12; + public static final int RESOURCE_ID_0 = 0; + public static final int RESOURCE_ID_1 = 1; + public static final int RESOURCE_ID_2 = 2; + public static final int RESOURCE_ID_3 = 3; + public static final int RESOURCE_ID_4 = 4; + public static final int RESOURCE_ID_7 = 7; + public static final int RESOURCE_ID_8 = 8; + public static final int RESOURCE_ID_9 = 9; + public static final int RESOURCE_ID_11 = 11; + public static final int RESOURCE_ID_14 = 14; + public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_INSTANCE_ID_2 = 2; - public static final String resourceIdName_3_9 = "batteryLevel"; - public static final String resourceIdName_3_14 = "UtfOffset"; - public static final String resourceIdName_19_0_0 = "dataRead"; - public static final String resourceIdName_19_1_0 = "dataWrite"; + public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel"; + public static final String RESOURCE_ID_NAME_3_14 = "UtfOffset"; + public static final String RESOURCE_ID_NAME_19_0_0 = "dataRead"; + public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite"; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 1c62b2b79d..af12168802 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -62,9 +62,9 @@ import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; @@ -99,12 +99,12 @@ public class LwM2MTestClient { initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); - initializer.setInstancesForObject(BINARY_APP_DATA_CONTAINER, lwM2MBinaryAppDataContainer = new LwM2mBinaryAppDataContainer(executor, objectInstanceId_0), - new LwM2mBinaryAppDataContainer(executor, objectInstanceId_1)); + initializer.setInstancesForObject(BINARY_APP_DATA_CONTAINER, lwM2MBinaryAppDataContainer = new LwM2mBinaryAppDataContainer(executor, OBJECT_INSTANCE_ID_0), + new LwM2mBinaryAppDataContainer(executor, OBJECT_INSTANCE_ID_1)); locationParams = new LwM2MLocationParams(); locationParams.getPos(); - initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, objectInstanceId_0)); - initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, objectInstanceId_0), new LwM2mTemperatureSensor(executor, objectInstanceId_12)); + initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); + initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0), new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12)); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); dtlsConfig.setRecommendedCipherSuitesOnly(true); @@ -130,16 +130,6 @@ public class LwM2MTestClient { ObservationStore store) { CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig); - - // tricks to be able to change psk information on the fly -// AdvancedPskStore pskStore = dtlsConfig.getAdvancedPskStore(); -// if (pskStore != null) { -// PskPublicInformation identity = pskStore.getIdentity(null, null); -// SecretKey key = pskStore -// .requestPskSecretResult(ConnectionId.EMPTY, null, identity, null, null, null).getSecret(); -// singlePSKStore = new SinglePSKStore(identity, key); -// dtlsConfigBuilder.setAdvancedPskStore(singlePSKStore); -// } builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build())); builder.setNetworkConfig(coapConfig); return builder.build(); @@ -283,5 +273,4 @@ public class LwM2MTestClient { client.start(); } } - } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java index b7ac889649..7da7599119 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java @@ -66,8 +66,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements * "value":4 * }, */ -// private String data = "InNlcnZpY2VJZCI6Ik1ldGVyIiwNCiJzZXJ2aWNlRGF0YSI6ew0KImN1cnJlbnRSZWFkaW5nIjoiNDYuMyIsDQoic2lnbmFsU3RyZW5ndGgiOjE2LA0KImRhaWx5QWN0aXZpdHlUaW1lIjo1NzA2DQo="; -// private byte[] data; + Map data; private Integer priority = 0; private Time timestamp; @@ -83,7 +82,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements try { if (id != null) this.setId(id); executorService.scheduleWithFixedDelay(() -> -// fireResourcesChange(0, 2), 5000, 5000, TimeUnit.MILLISECONDS); fireResourcesChange(0, 2), 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); @@ -93,15 +91,11 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements @Override public ReadResponse read(ServerIdentity identity, int resourceId) { -// log.warn("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); try { switch (resourceId) { case 0: -// log.warn("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); ReadResponse response = ReadResponse.success(resourceId, getData(), ResourceModel.Type.OPAQUE); -// log.warn("Response [{}]", response); return response; - case 1: return ReadResponse.success(resourceId, getPriority()); case 2: @@ -168,7 +162,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } private String getDataFormat() { -// return this.dataFormat == null ? "base64" : this.dataFormat; return this.dataFormat == null ? "OPAQUE" : this.dataFormat; } @@ -188,7 +181,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements return this.timestamp != null ? this.timestamp : new Time(new Date().getTime()); } -// fireResourcesChange(resourceId); private boolean setData(LwM2mResource value, boolean replace) { try { if (value instanceof LwM2mMultipleResource) { @@ -208,7 +200,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } private Map getData() { -// this.data.put(23, new byte[]{0,0, 2,3}); return data; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java index 079cd9fdac..b4dd8531f2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java @@ -101,19 +101,15 @@ public class LwM2mLocation extends BaseInstanceEnabler implements Destroyable { switch (nextMove.charAt(0)) { case 'w': moveLatitude(1.0f); -// log.info("Move to North [{}]/[{}]", getLatitude(), getLongitude()); break; case 'a': moveLongitude(-1.0f); -// log.info("Move to East [{}]/[{}]", getLatitude(), getLongitude()); break; case 's': moveLatitude(-1.0f); -// log.info("Move to South [{}]/[{}]", getLatitude(), getLongitude()); break; case 'd': moveLongitude(1.0f); -// log.info("Move to West [{}]/[{}]", getLatitude(), getLongitude()); break; } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index 6cf35aeb94..a4e5e92db4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -29,13 +29,13 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; @DaoSqlTest public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { - private final String[] resources = new String[]{"3.xml", "5.xml", "9.xml"}; + private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml"}; protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; public AbstractOtaLwM2MIntegrationTest() { - setResources(this.resources); + setResources(this.RESOURCES_OTA); } protected OtaPackageInfo createFirmware() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 3b152ec728..38db441e5d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,7 +138,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { - createDeviceProfile(transportConfiguration); + createDeviceProfile(TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 4303c5e6eb..fba8109dbf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -34,16 +34,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.COAP_CONFIG; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SECURITY; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_0_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_1_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; @DaoSqlTest @@ -60,13 +60,13 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectInstanceIdVer_1; protected String objectIdVer_0; protected String objectIdVer_2; - private static final Predicate predicate_3 = path -> (!((String) path).contains("/" + TEMPERATURE_SENSOR) && ((String) path).contains("/" + DEVICE)); + private static final Predicate PREDICATE_3 = path -> (!((String) path).contains("/" + TEMPERATURE_SENSOR) && ((String) path).contains("/" + DEVICE)); protected String objectIdVer_3; protected String objectInstanceIdVer_3; protected String objectInstanceIdVer_5; protected String objectInstanceIdVer_9; protected String objectIdVer_19; - protected String objectIdVer_50 = "/50"; + protected final String OBJECT_ID_VER_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; @@ -100,19 +100,19 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg }); } }); - String ver_Id_0 = client.getClient().getObjectTree().getModel().getObjectModel(objectId_0).version; + String ver_Id_0 = client.getClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; if ("1.0".equals(ver_Id_0)) { - objectIdVer_0 = "/" + objectId_0; + objectIdVer_0 = "/" + OBJECT_ID_0; } else { - objectIdVer_0 = "/" + objectId_0 + "_" + ver_Id_0; + objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; } objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + ACCESS_CONTROL)).findFirst().get(); - objectIdVer_3 = (String) expectedObjects.stream().filter(predicate_3).findFirst().get(); + objectIdVer_3 = (String) expectedObjects.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); objectIdVer_3303 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + TEMPERATURE_SENSOR)).findFirst().get(); objectInstanceIdVer_1 = (String) expectedObjectIdVerInstances.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get(); - objectInstanceIdVer_3 = (String) expectedObjectIdVerInstances.stream().filter(predicate_3).findFirst().get(); + objectInstanceIdVer_3 = (String) expectedObjectIdVerInstances.stream().filter(PREDICATE_3).findFirst().get(); objectInstanceIdVer_5 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).contains("/" + FIRMWARE)).findFirst().get(); objectInstanceIdVer_9 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).contains("/" + SOFTWARE_MANAGEMENT)).findFirst().get(); @@ -120,22 +120,22 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " \"type\": \"LWM2M\",\n" + " \"observeAttr\": {\n" + " \"keyName\": {\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\": \"" + resourceIdName_3_9 + "\",\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_14 + "\": \"" + resourceIdName_3_14 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\": \"" + resourceIdName_19_0_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "\": \"" + resourceIdName_19_1_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\"\n" + " },\n" + " \"observe\": [\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\"\n" + " ],\n" + " \"attribute\": [\n" + " ],\n" + " \"telemetry\": [\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\",\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_14 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\",\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" + " ],\n" + " \"attributeLwm2m\": {}\n" + " },\n" + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java index 4ab0d68794..40d26bfc17 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java @@ -25,10 +25,10 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTest { @@ -43,8 +43,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdByIdKey_Result_CREATED() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_12; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_12; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CREATED.getName(), rpcActualResult.get("result").asText()); @@ -60,12 +60,12 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdAlreadyExistsById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "instance " + objectInstanceId_0 + " already exists"; + String expected = "instance " + OBJECT_INSTANCE_ID_0 + " already exists"; String actual = rpcActualResult.get("error").asText(); assertTrue(actual.equals(expected)); } @@ -77,8 +77,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdMandatorySingleObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}}"; + String expectedPath = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -94,8 +94,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdSecurityObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_0 + "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"2\":4}}"; + String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"2\":4}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -113,8 +113,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdAbsentObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_50+ "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}}"; + String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index ebc0f6d783..099477d216 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -24,10 +24,9 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_7; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTest { @@ -39,7 +38,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteObjectInstanceIsSuchByIdKey_Result_DELETED() throws Exception { - String expectedPath = objectIdVer_3303 + "/" + objectInstanceId_12; + String expectedPath = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.DELETED.getName(), rpcActualResult.get("result").asText()); @@ -52,7 +51,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteObjectInstanceIsNotSuchByIdKey_Result_NOT_FOUND() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_12; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_12; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -82,7 +81,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_7; + String expectedPath = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + RESOURCE_ID_7; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index b8ca7be61f..ede501c4a2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -31,8 +31,8 @@ import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest { @@ -141,7 +141,7 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration */ @Test public void testDiscoverObjectInstanceAbsentInObject_Return_NOT_FOUND() throws Exception { - String expected = objectIdVer_2 + "/" + objectInstanceId_0; + String expected = objectIdVer_2 + "/" + OBJECT_INSTANCE_ID_0; String actualResult = sendDiscover(expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -152,7 +152,7 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration */ @Test public void testDiscoverResourceAbsentInObject_Return_NOT_FOUND() throws Exception { - String expected = objectIdVer_2 + "/" + objectInstanceId_0 + "/" + resourceId_2; + String expected = objectIdVer_2 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; String actualResult = sendDiscover(expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index 73ea0fb74c..4d13cbd386 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -25,12 +25,12 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_4; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_8; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_4; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_8; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationTest { @@ -43,7 +43,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteUpdateFWById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_5 + "/" + resourceId_2; + String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -56,7 +56,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteUpdateSWById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_9 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_9 + "/" + RESOURCE_ID_4; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -69,7 +69,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteRebootById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -82,7 +82,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteRegistrationUpdateTriggerById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_1 + "/" + resourceId_8; + String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_8; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -96,7 +96,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteResourceWithParametersById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; Object expectedValue = 60; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -110,7 +110,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServerConfigured() throws Exception { - String expectedPath = objectInstanceIdVer_1 + "/" + resourceId_9; + String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_9; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -126,7 +126,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteResourceWithOperationNotExecuteById_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectInstanceIdVer_5 + "/" + resourceId_3; + String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -143,7 +143,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_50 + "/" + objectInstanceId_0 + "/" + resourceId_3; + String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -161,7 +161,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteSecurityObjectById_Result_NOT_FOUND() throws Exception { - String expectedPath = objectIdVer_0 + "/" + objectInstanceId_0 + "/" + resourceId_3; + String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 848a9563ed..f6b53c8c07 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -27,10 +27,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { @@ -55,7 +55,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveSingleResource_Result_CONTENT_Value_SingleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; String actualResult = sendObserve("Observe", expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -87,7 +87,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveNoImplementedInstanceOnDevice_Result_NotFound() throws Exception { String objectInstanceIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String)path).contains("/" + ACCESS_CONTROL)).findFirst().get(); - String expected = objectInstanceIdVer + "/" + objectInstanceId_0; + String expected = objectInstanceIdVer + "/" + OBJECT_INSTANCE_ID_0; String actualResult = sendObserve("Observe", expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -101,7 +101,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveNoImplementedResourceOnDeviceValueNull_Result_BadRequest() throws Exception { String objectIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String)path).contains("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); - String expected = objectIdVer + "/" + objectInstanceId_0 + "/" + resourceId_0; + String expected = objectIdVer + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; String actualResult = sendObserve("Observe", expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String expectedValue = "values MUST NOT be null"; @@ -116,7 +116,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveRSourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedId = objectInstanceIdVer_5 + "/" + resourceId_0; + String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0; sendObserve("Observe", expectedId); String actualResult = sendObserve("Observe", expectedId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -130,7 +130,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveRepeatedRequestObserveOnDevice_Result_BAD_REQUEST_ErrorMsg_AlreadyRegistered() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + resourceId_0; + String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; sendObserve("Observe", expectedId); String actualResult = sendObserve("Observe", expectedId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -146,8 +146,8 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveAll() throws Exception { sendObserve("ObserveCancelAll", null); - String expectedId_0 = objectInstanceIdVer_3 + "/" + resourceId_0; - String expectedId_9 = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedId_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String expectedId_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; sendObserve("Observe", expectedId_0); sendObserve("Observe", expectedId_9); String actualResult = sendObserve("ObserveReadAll", null); @@ -167,8 +167,8 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveCancelOneResource_Result_CONTENT_Value_Count_1() throws Exception { sendObserve("ObserveCancelAll", null); - String expectedId_0 = objectInstanceIdVer_3 + "/" + resourceId_0; - String expectedId_3 = objectInstanceIdVer_5 + "/" + resourceId_3; + String expectedId_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String expectedId_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; sendObserve("Observe", expectedId_0); sendObserve("Observe", expectedId_3); String actualResult = sendObserve("ObserveCancel", expectedId_0); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 9116659bc3..aece59d119 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -27,18 +27,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_0_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_1_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_11; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest { @@ -96,11 +96,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + resourceId_11 ; + String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_11; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mMultipleResource [id=" + resourceId_11 + ", values={"; + String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -109,11 +109,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + resourceId_14 ; + String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_14; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value="; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -122,11 +122,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadSingleResourceByKey_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { - String expectedKey = resourceIdName_3_14 ; + String expectedKey = RESOURCE_ID_NAME_3_14; String actualResult = sendRPCByKey(expectedKey); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value="; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -137,16 +137,16 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception { String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String)path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String)path).contains("/" + SERVER))).findFirst().get(); String objectId_1 = pathIdVerToObjectId(expectedIdVer_1); - String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + resourceId_1; - String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + resourceId_11; + String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1; + String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3); String expectedIds = "[\"" + expectedIdVer_1 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]"; String actualResult = sendCompositeRPCByIds(expectedIds); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected1 = objectId_1 + "=LwM2mObject [id=" + new LwM2mPath(objectId_1).getObjectId() + ", instances={"; - String expected3_0_1 = objectInstanceId_3 + "/" + resourceId_1 + "=LwM2mSingleResource [id=" + resourceId_1 + ", value="; - String expected3_0_11 = objectInstanceId_3 + "/" + resourceId_11 + "=LwM2mMultipleResource [id=" + resourceId_11 + ", values={"; + String expected3_0_1 = objectInstanceId_3 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value="; + String expected3_0_11 = objectInstanceId_3 + "/" + RESOURCE_ID_11 + "=LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected1)); assertTrue(actualValues.contains(expected3_0_1)); @@ -159,8 +159,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectInstanceIsLwM2mSingleResource() throws Exception { String expectedIdVer3_0 = objectInstanceIdVer_3; - String expectedIdVer1_0_1 = objectInstanceIdVer_1 + "/" + resourceId_1; - String expectedIdVer1_0_2 = objectInstanceIdVer_1 + "/" + resourceId_2; + String expectedIdVer1_0_1 = objectInstanceIdVer_1 + "/" + RESOURCE_ID_1; + String expectedIdVer1_0_2 = objectInstanceIdVer_1 + "/" + RESOURCE_ID_2; String expectedIds = "[\"" + expectedIdVer1_0_1 + "\", \"" + expectedIdVer1_0_2 + "\", \"" + expectedIdVer3_0 + "\"]"; String actualResult = sendCompositeRPCByIds(expectedIds); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -169,8 +169,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest LwM2mPath path = new LwM2mPath(objectInstanceId_3); String expected3_0 = objectInstanceId_3 + "=LwM2mObjectInstance [id=" + path.getObjectInstanceId() + ", resources={"; String objectInstanceId_1 = pathIdVerToObjectId(objectInstanceIdVer_1); - String expected1_0_1 = objectInstanceId_1 + "/" + resourceId_1 + "=LwM2mSingleResource [id=" + resourceId_1 + ", value="; - String expected1_0_2 = objectInstanceId_1 + "/" + resourceId_2 + "=null"; + String expected1_0_1 = objectInstanceId_1 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value="; + String expected1_0_2 = objectInstanceId_1 + "/" + RESOURCE_ID_2 + "=null"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected3_0)); assertTrue(actualValues.contains(expected1_0_1)); @@ -182,20 +182,20 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadCompositeSingleResourceByKeys_Result_CONTENT_Value_3_0_IsLwM2mSingleResource_19_0_0_AND_19_0_1_Null() throws Exception { - String expectedKey3_0_9 = resourceIdName_3_9; - String expectedKey3_0_14 = resourceIdName_3_14; - String expectedKey19_0_0 = resourceIdName_19_0_0; - String expectedKey19_1_0 = resourceIdName_19_1_0; + String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; + String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; + String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; String actualResult = sendCompositeRPCByKeys(expectedKeys); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3); String objectId_19 = pathIdVerToObjectId(objectIdVer_19); - String expected3_0_9 = objectInstanceId_3 + "/" + resourceId_9 + "=LwM2mSingleResource [id=" + resourceId_9 + ", value="; - String expected3_0_14 = objectInstanceId_3 + "/" + resourceId_14 + "=LwM2mSingleResource [id=" + resourceId_14 + ", value="; - String expected19_0_0 = objectId_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "=null"; - String expected19_1_0 = objectId_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "=null"; + String expected3_0_9 = objectInstanceId_3 + "/" + RESOURCE_ID_9 + "=LwM2mSingleResource [id=" + RESOURCE_ID_9 + ", value="; + String expected3_0_14 = objectInstanceId_3 + "/" + RESOURCE_ID_14 + "=LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; + String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "=null"; + String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "=null"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected3_0_9)); assertTrue(actualValues.contains(expected3_0_14)); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index ea23ab48b9..531fa33dc9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.leshan.core.ResponseCode; -import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; @@ -25,13 +24,7 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_4; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_8; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { @@ -45,7 +38,7 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte */ @Test public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index 1a92831a36..f5f06631b9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -25,16 +25,15 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_15; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceInstanceId_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_2; public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTest { @@ -46,7 +45,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedValue = "+12"; String actualResult = sendRPCWriteStringById("WriteReplace", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -54,7 +53,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadById(expectedPath); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -65,7 +64,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceByKey_Result_CHANGED() throws Exception { - String expectedKey = resourceIdName_3_14; + String expectedKey = RESOURCE_ID_NAME_3_14; String expectedValue = "+09"; String actualResult = sendRPCWriteByKey("WriteReplace", expectedKey, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -73,7 +72,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadByKey(expectedKey); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -85,7 +84,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueMultipleResource_Result_CHANGED_Value_Multi_Instance_Resource_must_in_Json_format() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; int resourceInstanceId0 = 0; int resourceInstanceId15 = 15; String expectedValue0 = "0000ad45675600"; @@ -115,7 +114,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceR_ById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; Integer expectedValue = 90; String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -132,21 +131,21 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes String expectedPath = objectInstanceIdVer_3; String expectedValue14 = "+5"; String expectedValue15 = "Kiyv/Europe"; - String expectedValue = "{\"" + resourceId_14 + "\":\"" + expectedValue14 + "\",\"" + resourceId_15 + "\":\"" + expectedValue15 + "\"}"; + String expectedValue = "{\"" + RESOURCE_ID_14 + "\":\"" + expectedValue14 + "\",\"" + RESOURCE_ID_15 + "\":\"" + expectedValue15 + "\"}"; String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); - String expectedPath14 = objectInstanceIdVer_3 + "/" + resourceId_14; - String expectedPath15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedPath15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; actualResult = sendRPCReadById(expectedPath14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue14 + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -157,17 +156,17 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteUpdateValueMultipleResourceById_Result_CHANGED() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; int resourceInstanceId0 = 0; int resourceInstanceId25 = 25; String expectedValue0 = "00ad45675600"; String expectedValue25 = "25ad45675600cdef"; - String expectedValue = "{\"" + resourceId_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); - String expectedPath0 = expectedPath + "/" + resourceId_0 + "/" + resourceInstanceId0; - String expectedPath25 =expectedPath + "/" + resourceId_0 + "/" + resourceInstanceId25; + String expectedPath0 = expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0; + String expectedPath25 =expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25; actualResult = sendRPCReadById(expectedPath0); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); @@ -188,11 +187,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes @Test public void testWriteCompositeValueSingleResourceResourceInstanceByIdKey_Result_CHANGED() throws Exception { int resourceInstanceId2 = 2; - String expectedPath19_1_0_2 = objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "/" + resourceInstanceId2; + String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; @@ -207,12 +206,12 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadByKey(expectedKey3_0_14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath3_0_15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -246,11 +245,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteCompositeCreateResourceInstanceUpdateSingleResourceByIdKey_Result_CHANGED() throws Exception { - String expectedPath19_1_0_2 = objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "/" + resourceInstanceId_2; + String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; @@ -260,17 +259,17 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadById(expectedPath19_1_0_2); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mResourceInstance [id=" + resourceInstanceId_2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]"; + String expected = "LwM2mResourceInstance [id=" + RESOURCE_INSTANCE_ID_2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadByKey(expectedKey3_0_14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath3_0_15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -285,11 +284,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteCompositeCreateObjectInstanceUpdateSingleResourceByIdKey_Result_BAD_REQUEST() throws Exception { - String expectedPath19_1_2_2 = objectIdVer_19 + "/" + objectInstanceId_2 + "/" + resourceId_0 + "/" + resourceInstanceId_2; + String expectedPath19_1_2_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_2 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_2_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index c9d2b556af..9108366395 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -62,7 +62,7 @@ protected final X509Certificate serverX509Cert; protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK protected final PublicKey clientPublicKeyFromCertTrustNo; // client public key used for RPK - private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; @@ -71,7 +71,7 @@ protected final X509Certificate serverX509Cert; public AbstractSecurityLwM2MIntegrationTest() { // create client credentials - setResources(this.resources); + setResources(this.RESOURCES_SECURITY); try { // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); From 1b488781d5690093e968fec4f6c6adb5f852af00 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 19:07:33 +0200 Subject: [PATCH 079/403] lwm2m for profile bootstrap need input: X509 certificate (instead of X509 public key) --- .../server/service/lwm2m/LwM2MServiceImpl.java | 18 ++++++++++++++++++ .../bootstrap/LwM2MServerSecurityConfig.java | 14 ++++++++++---- .../dao/device/DeviceProfileServiceImpl.java | 6 +++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java index b5ec5cc654..7d0c623f96 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java @@ -63,6 +63,12 @@ public class LwM2MServiceImpl implements LwM2MService { } else { bsServ.setServerPublicKey(Base64.encodeBase64String(publicKeyBase64)); } + byte[] certificateBase64 = getCertificate(bsServerConfig); + if (certificateBase64 == null) { + bsServ.setServerCertificate(""); + } else { + bsServ.setServerCertificate(Base64.encodeBase64String(certificateBase64)); + } return bsServ; } @@ -77,5 +83,17 @@ public class LwM2MServiceImpl implements LwM2MService { } return null; } + + private byte[] getCertificate(LwM2MSecureServerConfig config) { + try { + SslCredentials sslCredentials = config.getSslCredentials(); + if (sslCredentials != null) { + return sslCredentials.getCertificateChain()[0].getEncoded(); + } + } catch (Exception e) { + log.trace("Failed to fetch certificate from key store!", e); + } + return null; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java index 12dd9c5a64..11bf049621 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java @@ -42,17 +42,23 @@ public class LwM2MServerSecurityConfig { @ApiModelProperty(position = 8, value = "Server Public Key for 'Security' mode (DTLS): RPK or X509. Format: base64 encoded", example = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAZ0pSaGKHk/GrDaUDnQZpeEdGwX7m3Ws+U/kiVat\n" + "+44sgk3c8g0LotfMpLlZJPhPwJ6ipXV+O1r7IZUjBs3LNA==", readOnly = true) protected String serverPublicKey; - @ApiModelProperty(position = 9, value = "Bootstrap Server Account Timeout (If the value is set to 0, or if this resource is not instantiated, the Bootstrap-Server Account lifetime is infinite.)", example = "0", readOnly = true) + @ApiModelProperty(position = 9, value = "Server Public Key for 'Security' mode (DTLS): X509. Format: base64 encoded", example = "MMIICODCCAd6gAwIBAgIUI88U1zowOdrxDK/dOV+36gJxI2MwCgYIKoZIzj0EAwIwejELMAkGA1UEBhMCVUs\n" + + "xEjAQBgNVBAgTCUt5aXYgY2l0eTENMAsGA1UEBxMES3lpdjEUMBIGA1UEChMLVGhpbmdzYm9hcmQxFzAVBgNVBAsMDkRFVkVMT1BFUl9URVNUMRkwFwYDVQQDDBBpbnRlcm1lZGlhdGVfY2EwMB4XDTIyMDEwOTEzMDMwMFoXDTI3MDEwODEzMDMwMFowFDESMBAGA1UEAxM\n" + + "JbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUO3vBo/JTv0eooY7XHiKAIVDoWKFqtrU7C6q8AIKqpLcqhCdW+haFeBOH3PjY6EwaWkY04Bir4oanU0s7tz2uKOBpzCBpDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/\n" + + "BAIwADAdBgNVHQ4EFgQUEjc3Q4a0TxzP/3x3EV4fHxYUg0YwHwYDVR0jBBgwFoAUuSquGycMU6Q0SYNcbtSkSD3TfH0wLwYDVR0RBCgwJoIVbG9jYWxob3N0LmxvY2FsZG9tYWlugglsb2NhbGhvc3SCAiAtMAoGCCqGSM49BAMCA0gAMEUCIQD7dbZObyUaoDiNbX+9fUNp\n" + + "AWrD7N7XuJUwZ9FcN75R3gIgb2RNjDkHoyUyF1YajwkBk+7XmIXNClmizNJigj908mw=", readOnly = true) + protected String serverCertificate; + @ApiModelProperty(position = 10, value = "Bootstrap Server Account Timeout (If the value is set to 0, or if this resource is not instantiated, the Bootstrap-Server Account lifetime is infinite.)", example = "0", readOnly = true) Integer bootstrapServerAccountTimeout = 0; /** Config -> ObjectId = 1 'LwM2M Server' */ - @ApiModelProperty(position = 10, value = "Specify the lifetime of the registration in seconds.", example = "300", readOnly = true) + @ApiModelProperty(position = 11, value = "Specify the lifetime of the registration in seconds.", example = "300", readOnly = true) private Integer lifetime = 300; - @ApiModelProperty(position = 11, value = "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation. " + + @ApiModelProperty(position = 12, value = "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation. " + "If this Resource doesn’t exist, the default value is 0.", example = "1", readOnly = true) private Integer defaultMinPeriod = 1; /** ResourceID=6 'Notification Storing When Disabled or Offline' */ - @ApiModelProperty(position = 12, value = "If true, the LwM2M Client stores “Notify” operations to the LwM2M Server while the LwM2M Server account is disabled or the LwM2M Client is offline. After the LwM2M Server account is enabled or the LwM2M Client is online, the LwM2M Client reports the stored “Notify” operations to the Server. " + + @ApiModelProperty(position = 13, value = "If true, the LwM2M Client stores “Notify” operations to the LwM2M Server while the LwM2M Server account is disabled or the LwM2M Client is offline. After the LwM2M Server account is enabled or the LwM2M Client is online, the LwM2M Client reports the stored “Notify” operations to the Server. " + "If false, the LwM2M Client discards all the “Notify” operations or temporarily disables the Observe function while the LwM2M Server is disabled or the LwM2M Client is offline. " + "The default value is true.", example = "true", readOnly = true) private boolean notifIfDisabled = true; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 18a8a1ecde..b685fc7aa8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -765,15 +765,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D X509LwM2MBootstrapServerCredential x509ServerCredentials = (X509LwM2MBootstrapServerCredential) bootstrapServerConfig; server = x509ServerCredentials.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; if (StringUtils.isEmpty(x509ServerCredentials.getServerPublicKey())) { - throw new DeviceCredentialsValidationException(server + " X509 public key must be specified!"); + throw new DeviceCredentialsValidationException(server + " X509 certificate must be specified!"); } try { String certServer = EncryptionUtil.certTrimNewLines(x509ServerCredentials.getServerPublicKey()); x509ServerCredentials.setServerPublicKey(certServer); - SecurityUtil.publicKey.decode(x509ServerCredentials.getDecodedCServerPublicKey()); + SecurityUtil.certificate.decode(x509ServerCredentials.getDecodedCServerPublicKey()); } catch (Exception e) { - throw new DeviceCredentialsValidationException(server + " X509 public key must be in standard [RFC7250] and then encoded to Base64 format!"); + throw new DeviceCredentialsValidationException(server + " X509 certificate must be in DER-encoded X509v3 format and support only EC algorithm and then encoded to Base64 format!"); } break; } From e4579d21d788db8a0784a7dd9bf68383683cd216 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Jan 2022 11:01:29 +0200 Subject: [PATCH 080/403] UI: Added persistent page link models and subscribe to router query params --- .../entity/entities-table.component.ts | 53 ++++++++++++------- .../src/app/shared/models/page/page-link.ts | 6 +++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 8fd2f8a976..11b0ea0488 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -31,12 +31,12 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { MAX_SAFE_PAGE_SIZE, PageLink, TimePageLink } from '@shared/models/page/page-link'; +import { MAX_SAFE_PAGE_SIZE, PageLink, PageQueryParam, TimePageLink } from '@shared/models/page/page-link'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; -import { MatSort } from '@angular/material/sort'; +import { MatSort, SortDirection } from '@angular/material/sort'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, skip, tap } from 'rxjs/operators'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @@ -194,7 +194,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.columnsUpdated(); - const routerQueryParams = this.route.snapshot.queryParams; + const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams; let sortOrder: SortOrder = null; if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) { @@ -219,10 +219,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; if (routerQueryParams.hasOwnProperty('page')) { - this.pageLink.page = routerQueryParams.page; + this.pageLink.page = Number(routerQueryParams.page); } if (routerQueryParams.hasOwnProperty('pageSize')) { - this.pageLink.pageSize = routerQueryParams.pageSize; + this.pageLink.pageSize = Number(routerQueryParams.pageSize); } if (routerQueryParams.hasOwnProperty('textSearch') && !isEmptyStr(routerQueryParams.textSearch)) { this.textSearchMode = true; @@ -249,19 +249,33 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa debounceTime(150), distinctUntilChanged(), tap(() => { - const queryParams: any = { + const queryParams: PageQueryParam = { textSearch: encodeURI(this.pageLink.textSearch) || null }; if (this.displayPagination) { this.paginator.pageIndex = 0; queryParams.page = null; } - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); }) ) .subscribe(); + this.route.queryParams.pipe(skip(1)).subscribe((params: PageQueryParam) => { + this.paginator.pageIndex = Number(params.page) || 0; + this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize; + this.sort.active = params.property || this.entitiesTableConfig.defaultSortOrder.property; + this.sort.direction = (params.direction || this.entitiesTableConfig.defaultSortOrder.direction).toLowerCase() as SortDirection; + if (params.hasOwnProperty('textSearch') && !isEmptyStr(params.textSearch)) { + this.textSearchMode = true; + this.pageLink.textSearch = decodeURI(params.textSearch); + } else { + this.textSearchMode = false; + this.pageLink.textSearch = null; + } + this.updateData(); + }); + this.updatePaginationSubscriptions(); this.viewInited = true; } @@ -275,8 +289,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortSubscription$: Observable = this.sort.sortChange.asObservable().pipe( map((data) => { const direction = data.direction.toUpperCase(); - const queryParams: any = { - direction: this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction, + const queryParams: PageQueryParam = { + direction: (this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction) as Direction, property: this.entitiesTableConfig?.defaultSortOrder?.property === data.active ? null : data.active }; if (this.displayPagination) { @@ -297,10 +311,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa ); } this.updateDataSubscription = ((this.displayPagination ? merge(sortSubscription$, paginatorSubscription$) - : sortSubscription$) as Observable).pipe( + : sortSubscription$) as Observable).pipe( tap((queryParams) => { - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); }) ).subscribe(); } @@ -486,15 +499,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa exitFilterMode() { this.textSearchMode = false; this.pageLink.textSearch = null; - const queryParams: any = { + const queryParams: PageQueryParam = { textSearch: null }; if (this.displayPagination) { this.paginator.pageIndex = 0; queryParams.page = null; } - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); } resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) { @@ -508,9 +520,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); this.sort.active = sortable.id; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; - this.updatedRouterQueryParams({}, 'preserve'); if (update) { - this.updateData(); + this.updatedRouterParamsAndData({}, 'preserve'); } } @@ -628,13 +639,15 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return entity.id.id; } - protected updatedRouterQueryParams(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { + protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { if (this.persistentPageLinkMode) { this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling }); + } else { + this.updateData(); } } } diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index 92fe4634f5..2528b6ce99 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -23,6 +23,12 @@ export const MAX_SAFE_PAGE_SIZE = 2147483647; export type PageLinkSearchFunction = (entity: T, textSearch: string, searchProperty?: string) => boolean; +export interface PageQueryParam extends Partial{ + textSearch?: string; + pageSize?: number; + page?: number; +} + export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { return (entity, textSearch) => defaultPageLinkSearch(entity, textSearch, searchProperty); } From b458dddc56c2db52615e5f2ebc1421da9a7d4074 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 12 Jan 2022 11:20:18 +0200 Subject: [PATCH 081/403] Restore alarm relations update script. Add Bootstrap upgrade script --- .../install/SqlDatabaseUpgradeService.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) 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 eef29026ba..4a6d64caea 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 @@ -475,12 +475,30 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Updating schema ..."); schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); - log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); - conn.createStatement().execute("call update_profile_bootstrap();"); - log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); - log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); - conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); - log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + try { + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;"); + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select a.tenant_id, r.from_id, created_time, type, customer_id, id" + + " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;"); + conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';"); + } catch (Exception e) { + log.error("Failed to update alarm relations!!!", e); + } + + log.info("Updating lwm2m device profiles ..."); + try { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); + conn.createStatement().execute("call update_profile_bootstrap();"); + log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); + log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); + conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); + log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + } catch (Exception e) { + log.error("Failed to update lwm2m profiles!!!", e); + } log.info("Updating schema settings..."); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;"); log.info("Schema updated."); From 08997d682c0c69392cd268be8633408d6a2b325f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 12 Jan 2022 12:23:43 +0200 Subject: [PATCH 082/403] Don't allow sysadmin to delete himself --- .../org/thingsboard/server/controller/UserController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e83bc8970c..c7369fa5b9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -302,6 +302,10 @@ public class UserController extends BaseController { UserId userId = new UserId(toUUID(strUserId)); User user = checkUserId(userId, Operation.DELETE); + if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) { + throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED); + } + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), userId); userService.deleteUser(getCurrentUser().getTenantId(), userId); From b84ab58d7b41f307d8d8a4cd99f160c5a73dc330 Mon Sep 17 00:00:00 2001 From: Sergey Tarnavskiy Date: Wed, 12 Jan 2022 12:56:02 +0200 Subject: [PATCH 083/403] LwM2M transport. Added serverCertificate value for X509-securityMode in Bootstrap-config. --- .../device/lwm2m/lwm2m-device-config-server.component.ts | 3 +++ .../profile/device/lwm2m/lwm2m-profile-config.models.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index fa6d31153c..de75ea416b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -112,6 +112,9 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc this.changeSecurityHostPortFields(serverSecurityConfig); } this.serverFormGroup.patchValue(serverSecurityConfig, {emitEvent: false}); + if (this.currentSecurityMode === Lwm2mSecurityType.X509) { + this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false}); + } }); this.serverFormGroup.valueChanges.pipe( takeUntil(this.destroy$) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 56ae00c5a9..ce2824fb12 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -129,6 +129,7 @@ export interface ServerSecurityConfig { securityHost?: string; securityPort?: number; serverPublicKey?: string; + serverCertificate?: string; clientHoldOffTime?: number; shortServerId?: number; bootstrapServerAccountTimeout: number; From ab76d5a7a6c84053a93013c01237a13e84a0d5d0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 12 Jan 2022 13:59:13 +0200 Subject: [PATCH 084/403] fixed timescale agg by timezone --- .../dao/sqlts/timescale/AggregationRepository.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index 6559ea2c9e..8628d3e160 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java @@ -38,15 +38,15 @@ public class AggregationRepository { public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE tskv.entity_id = cast(:entityId AS uuid) AND tskv.key= cast(:entityKey AS int) AND tskv.ts > :startTs AND tskv.ts <= :endTs GROUP BY tskv.entity_id, tskv.key, tsBucket ORDER BY tskv.entity_id, tskv.key, tsBucket"; - public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; + public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; - public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; + public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; - public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; + public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; - public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; + public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; - public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount "; + public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount "; @PersistenceContext private EntityManager entityManager; From 6c166b90773092db02c702e24cd7739abddbec8e Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 12 Jan 2022 11:20:18 +0200 Subject: [PATCH 085/403] Restore alarm relations update script. Add Bootstrap upgrade script # Conflicts: # application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java --- .../install/SqlDatabaseUpgradeService.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) 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 c98e46d86e..6f142bab4c 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 @@ -1,12 +1,12 @@ /** * Copyright © 2016-2021 The Thingsboard Authors - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * 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. @@ -475,14 +475,30 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Updating schema ..."); schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); - log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); - loadSql(schemaUpdateFile, conn); - conn.createStatement().execute("call update_profile_bootstrap();"); - log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); - log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); - conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); - log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + try { + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;"); + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select a.tenant_id, r.from_id, created_time, type, customer_id, id" + + " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;"); + conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';"); + } catch (Exception e) { + log.error("Failed to update alarm relations!!!", e); + } + + log.info("Updating lwm2m device profiles ..."); + try { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); + conn.createStatement().execute("call update_profile_bootstrap();"); + log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); + log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); + conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); + log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + } catch (Exception e) { + log.error("Failed to update lwm2m profiles!!!", e); + } log.info("Updating schema settings..."); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;"); log.info("Schema updated."); From 69f060a48ce37cfc757ca93ca6bba4c975c1c9ad Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 12 Jan 2022 14:50:16 +0200 Subject: [PATCH 086/403] - license --- .../server/service/install/SqlDatabaseUpgradeService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 6f142bab4c..4a6d64caea 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 @@ -1,12 +1,12 @@ /** * Copyright © 2016-2021 The Thingsboard Authors - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * 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. From 949d3065609ad712df8df7ea5af03abea746679b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 12 Jan 2022 15:42:43 +0200 Subject: [PATCH 087/403] Added functionality to support 3.3.0 edge version rule chains --- .../service/edge/rpc/EdgeGrpcSession.java | 9 +- .../constructor/RuleChainMsgConstructor.java | 187 ++++++++- .../rpc/processor/RuleChainEdgeProcessor.java | 5 +- .../RuleChainMsgConstructorTest.java | 358 ++++++++++++++++++ common/edge-api/src/main/proto/edge.proto | 6 + 5 files changed, 543 insertions(+), 22 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 46b36ca99f..2fc8cdf841 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -52,6 +52,7 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EdgeUpdateMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -73,14 +74,11 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher; import java.io.Closeable; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; @@ -109,6 +107,8 @@ public final class EdgeGrpcSession implements Closeable { private boolean connected; private boolean syncCompleted; + private EdgeVersion edgeVersion; + private ScheduledExecutorService sendDownlinkExecutorService; EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener, @@ -525,7 +525,7 @@ public final class EdgeGrpcSession implements Closeable { case RULE_CHAIN: return ctx.getRuleChainProcessor().processRuleChainToEdge(edge, edgeEvent, msgType, action); case RULE_CHAIN_METADATA: - return ctx.getRuleChainProcessor().processRuleChainMetadataToEdge(edgeEvent, msgType); + return ctx.getRuleChainProcessor().processRuleChainMetadataToEdge(edgeEvent, msgType, this.edgeVersion); case ALARM: return ctx.getAlarmProcessor().processAlarmToEdge(edge, edgeEvent, msgType, action); case USER: @@ -655,6 +655,7 @@ public final class EdgeGrpcSession implements Closeable { try { if (edge.getSecret().equals(request.getEdgeSecret())) { sessionOpenListener.accept(edge.getId(), this); + this.edgeVersion = request.getEdgeVersion(); return ConnectResponseMsg.newBuilder() .setResponseCode(ConnectResponseCode.ACCEPTED) .setErrorMsg("") diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java index 11b14a78dc..e564b921f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructor.java @@ -17,15 +17,18 @@ package org.thingsboard.server.service.edge.rpc.constructor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; @@ -36,6 +39,10 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.ArrayList; import java.util.List; +import java.util.NavigableSet; +import java.util.TreeSet; +import java.util.UUID; +import java.util.stream.Collectors; @Component @Slf4j @@ -43,6 +50,8 @@ import java.util.List; public class RuleChainMsgConstructor { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String RULE_CHAIN_INPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainInputNode"; + private static final String TB_RULE_CHAIN_OUTPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode"; public RuleChainUpdateMsg constructRuleChainUpdatedMsg(RuleChainId edgeRootRuleChainId, UpdateMsgType msgType, RuleChain ruleChain) { RuleChainUpdateMsg.Builder builder = RuleChainUpdateMsg.newBuilder() @@ -60,18 +69,19 @@ public class RuleChainMsgConstructor { return builder.build(); } - public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { + public RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, + RuleChainMetaData ruleChainMetaData, + EdgeVersion edgeVersion) { try { - RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder() - .setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) - .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()) - .addAllNodes(constructNodes(ruleChainMetaData.getNodes())) - .addAllConnections(constructConnections(ruleChainMetaData.getConnections())) - .addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections())); - if (ruleChainMetaData.getFirstNodeIndex() != null) { - builder.setFirstNodeIndex(ruleChainMetaData.getFirstNodeIndex()); - } else { - builder.setFirstNodeIndex(-1); + RuleChainMetadataUpdateMsg.Builder builder = RuleChainMetadataUpdateMsg.newBuilder(); + switch (edgeVersion) { + case V_3_3_0: + constructRuleChainMetadataUpdatedMsg_V_3_3_0(builder, ruleChainMetaData); + break; + case V_3_3_3: + default: + constructRuleChainMetadataUpdatedMsg_V_3_3_3(builder, ruleChainMetaData); + break; } builder.setMsgType(msgType); return builder.build(); @@ -81,6 +91,104 @@ public class RuleChainMsgConstructor { return null; } + private void constructRuleChainMetadataUpdatedMsg_V_3_3_3(RuleChainMetadataUpdateMsg.Builder builder, + RuleChainMetaData ruleChainMetaData) throws JsonProcessingException { + builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()) + .addAllNodes(constructNodes(ruleChainMetaData.getNodes())) + .addAllConnections(constructConnections(ruleChainMetaData.getConnections())) + .addAllRuleChainConnections(constructRuleChainConnections(ruleChainMetaData.getRuleChainConnections(), new TreeSet<>())); + if (ruleChainMetaData.getFirstNodeIndex() != null) { + builder.setFirstNodeIndex(ruleChainMetaData.getFirstNodeIndex()); + } else { + builder.setFirstNodeIndex(-1); + } + } + + private void constructRuleChainMetadataUpdatedMsg_V_3_3_0(RuleChainMetadataUpdateMsg.Builder builder, + RuleChainMetaData ruleChainMetaData) throws JsonProcessingException { + List supportedNodes = filterNodes_V_3_3_0(ruleChainMetaData.getNodes()); + NavigableSet removedNodeIndexes = getRemovedNodeIndexes(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections()); + List connections = filterConnections_V_3_3_0(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections(), removedNodeIndexes); + + List ruleChainConnections = new ArrayList<>(); + if (ruleChainMetaData.getRuleChainConnections() != null) { + ruleChainConnections.addAll(ruleChainMetaData.getRuleChainConnections()); + } + ruleChainConnections.addAll(addRuleChainConnections_V_3_3_0(ruleChainMetaData.getNodes(), ruleChainMetaData.getConnections())); + builder.setRuleChainIdMSB(ruleChainMetaData.getRuleChainId().getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainMetaData.getRuleChainId().getId().getLeastSignificantBits()) + .addAllNodes(constructNodes(supportedNodes)) + .addAllConnections(constructConnections(connections)) + .addAllRuleChainConnections(constructRuleChainConnections(ruleChainConnections, removedNodeIndexes)); + if (ruleChainMetaData.getFirstNodeIndex() != null) { + Integer firstNodeIndex = ruleChainMetaData.getFirstNodeIndex(); + // decrease index because of removed nodes + for (Integer removedIndex : removedNodeIndexes) { + if (firstNodeIndex > removedIndex) { + firstNodeIndex = firstNodeIndex - 1; + } + } + builder.setFirstNodeIndex(firstNodeIndex); + } else { + builder.setFirstNodeIndex(-1); + } + } + + private List filterConnections_V_3_3_0(List nodes, List connections, NavigableSet removedNodeIndexes) { + List result = new ArrayList<>(); + if (connections != null) { + result = connections.stream().filter(conn -> { + for (int i = 0; i < nodes.size(); i++) { + RuleNode node = nodes.get(i); + if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE) + || node.getType().equalsIgnoreCase(TB_RULE_CHAIN_OUTPUT_NODE)) { + if (conn.getFromIndex() == i || conn.getToIndex() == i) { + return false; + } + } + } + return true; + }).map(conn -> { + NodeConnectionInfo newConn = new NodeConnectionInfo(); + newConn.setFromIndex(conn.getFromIndex()); + newConn.setToIndex(conn.getToIndex()); + newConn.setType(conn.getType()); + return newConn; + }).collect(Collectors.toList()); + } + + // decrease index because of removed nodes + for (Integer removedIndex : removedNodeIndexes) { + for (NodeConnectionInfo newConn : result) { + if (newConn.getToIndex() > removedIndex) { + newConn.setToIndex(newConn.getToIndex() - 1); + } + if (newConn.getFromIndex() > removedIndex) { + newConn.setFromIndex(newConn.getFromIndex() - 1); + } + } + } + + return result; + } + + private NavigableSet getRemovedNodeIndexes(List nodes, List connections) { + TreeSet removedIndexes = new TreeSet<>(); + for (NodeConnectionInfo connection : connections) { + for (int i = 0; i < nodes.size(); i++) { + RuleNode node = nodes.get(i); + if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE) + || node.getType().equalsIgnoreCase(TB_RULE_CHAIN_OUTPUT_NODE)) { + if (connection.getFromIndex() == i || connection.getToIndex() == i) { + removedIndexes.add(i); + } + } + } + } + return removedIndexes.descendingSet(); + } + private List constructConnections(List connections) { List result = new ArrayList<>(); if (connections != null && !connections.isEmpty()) { @@ -99,6 +207,21 @@ public class RuleChainMsgConstructor { .build(); } + private List filterNodes_V_3_3_0(List nodes) { + List result = new ArrayList<>(); + for (RuleNode node : nodes) { + switch (node.getType()) { + case RULE_CHAIN_INPUT_NODE: + case TB_RULE_CHAIN_OUTPUT_NODE: + log.trace("Skipping not supported rule node {}", node); + break; + default: + result.add(node); + } + } + return result; + } + private List constructNodes(List nodes) throws JsonProcessingException { List result = new ArrayList<>(); if (nodes != null && !nodes.isEmpty()) { @@ -109,23 +232,55 @@ public class RuleChainMsgConstructor { return result; } - private List constructRuleChainConnections(List ruleChainConnections) throws JsonProcessingException { + private List addRuleChainConnections_V_3_3_0(List nodes, List connections) throws JsonProcessingException { + List result = new ArrayList<>(); + for (int i = 0; i < nodes.size(); i++) { + RuleNode node = nodes.get(i); + if (node.getType().equalsIgnoreCase(RULE_CHAIN_INPUT_NODE)) { + for (NodeConnectionInfo connection : connections) { + if (connection.getToIndex() == i) { + RuleChainConnectionInfo e = new RuleChainConnectionInfo(); + e.setFromIndex(connection.getFromIndex()); + TbRuleChainInputNodeConfiguration configuration = JacksonUtil.treeToValue(node.getConfiguration(), TbRuleChainInputNodeConfiguration.class); + e.setTargetRuleChainId(new RuleChainId(UUID.fromString(configuration.getRuleChainId()))); + e.setAdditionalInfo(node.getAdditionalInfo()); + e.setType(connection.getType()); + result.add(e); + } + } + } + } + return result; + } + + private List constructRuleChainConnections(List ruleChainConnections, NavigableSet removedNodeIndexes) throws JsonProcessingException { List result = new ArrayList<>(); if (ruleChainConnections != null && !ruleChainConnections.isEmpty()) { for (RuleChainConnectionInfo ruleChainConnectionInfo : ruleChainConnections) { - result.add(constructRuleChainConnection(ruleChainConnectionInfo)); + result.add(constructRuleChainConnection(ruleChainConnectionInfo, removedNodeIndexes)); } } return result; } - private RuleChainConnectionInfoProto constructRuleChainConnection(RuleChainConnectionInfo ruleChainConnectionInfo) throws JsonProcessingException { + private RuleChainConnectionInfoProto constructRuleChainConnection(RuleChainConnectionInfo ruleChainConnectionInfo, NavigableSet removedNodeIndexes) throws JsonProcessingException { + int fromIndex = ruleChainConnectionInfo.getFromIndex(); + // decrease index because of removed nodes + for (Integer removedIndex : removedNodeIndexes) { + if (fromIndex > removedIndex) { + fromIndex = fromIndex - 1; + } + } + ObjectNode additionalInfo = (ObjectNode) ruleChainConnectionInfo.getAdditionalInfo(); + if (additionalInfo.get("ruleChainNodeId") == null) { + additionalInfo.put("ruleChainNodeId", "rule-chain-node-UNDEFINED"); + } return RuleChainConnectionInfoProto.newBuilder() - .setFromIndex(ruleChainConnectionInfo.getFromIndex()) + .setFromIndex(fromIndex) .setTargetRuleChainIdMSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getMostSignificantBits()) .setTargetRuleChainIdLSB(ruleChainConnectionInfo.getTargetRuleChainId().getId().getLeastSignificantBits()) .setType(ruleChainConnectionInfo.getType()) - .setAdditionalInfo(objectMapper.writeValueAsString(ruleChainConnectionInfo.getAdditionalInfo())) + .setAdditionalInfo(objectMapper.writeValueAsString(additionalInfo)) .build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java index 5ead8c5623..eb32340e87 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/RuleChainEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -63,14 +64,14 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { return downlinkMsg; } - public DownlinkMsg processRuleChainMetadataToEdge(EdgeEvent edgeEvent, UpdateMsgType msgType) { + public DownlinkMsg processRuleChainMetadataToEdge(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId); DownlinkMsg downlinkMsg = null; if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + ruleChainMsgConstructor.constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); if (ruleChainMetadataUpdateMsg != null) { downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java new file mode 100644 index 0000000000..4e8a21a8fa --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -0,0 +1,358 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.constructor; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.rule.NodeConnectionInfo; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@RunWith(MockitoJUnitRunner.class) +public class RuleChainMsgConstructorTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_3() throws JsonProcessingException { + RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); + RuleChainMsgConstructor constructor = new RuleChainMsgConstructor(); + RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + constructor.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData, EdgeVersion.V_3_3_3); + + Assert.assertEquals("First rule node index incorrect!", 3, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); + Assert.assertEquals("Nodes count incorrect!", 12, ruleChainMetadataUpdateMsg.getNodesCount()); + Assert.assertEquals("Connections count incorrect!", 13, ruleChainMetadataUpdateMsg.getConnectionsCount()); + Assert.assertEquals("Rule chain connections count incorrect!", 0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, "Other"), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(10)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(11)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, "RPC"), ruleChainMetadataUpdateMsg.getConnections(12)); + } + + @Test + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0() throws JsonProcessingException { + RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); + RuleChainMsgConstructor constructor = new RuleChainMsgConstructor(); + RuleChainMetaData ruleChainMetaData = createRuleChainMetaData(ruleChainId, 3, createRuleNodes(ruleChainId), createConnections()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + constructor.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData, EdgeVersion.V_3_3_0); + + Assert.assertEquals("First rule node index incorrect!", 2, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); + Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount()); + Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); + Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, "Other"), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + + RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); + Assert.assertEquals("From index incorrect!", 2, ruleChainConnection.getFromIndex()); + Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Additional info incorrect!", + "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", + ruleChainConnection.getAdditionalInfo()); + Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0); + Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0); + } + + @Test + public void testConstructRuleChainMetadataUpdatedMsg_V_3_3_0_inDifferentOrder() throws JsonProcessingException { + // same rule chain metadata, but different order of rule nodes + RuleChainId ruleChainId = new RuleChainId(UUID.randomUUID()); + RuleChainMsgConstructor constructor = new RuleChainMsgConstructor(); + RuleChainMetaData ruleChainMetaData1 = createRuleChainMetaData(ruleChainId, 8, createRuleNodesInDifferentOrder(ruleChainId), createConnectionsInDifferentOrder()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + constructor.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetaData1, EdgeVersion.V_3_3_0); + + Assert.assertEquals("First rule node index incorrect!", 7, ruleChainMetadataUpdateMsg.getFirstNodeIndex()); + Assert.assertEquals("Nodes count incorrect!", 10, ruleChainMetadataUpdateMsg.getNodesCount()); + Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); + Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); + + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, "Other"), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + + RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); + Assert.assertEquals("From index incorrect!", 7, ruleChainConnection.getFromIndex()); + Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Additional info incorrect!", + "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", + ruleChainConnection.getAdditionalInfo()); + Assert.assertTrue("Target rule chain id MSB incorrect!", ruleChainConnection.getTargetRuleChainIdMSB() != 0); + Assert.assertTrue("Target rule chain id LSB incorrect!", ruleChainConnection.getTargetRuleChainIdLSB() != 0); + } + + private void compareNodeConnectionInfoAndProto(NodeConnectionInfo expected, org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto actual) { + Assert.assertEquals(expected.getFromIndex(), actual.getFromIndex()); + Assert.assertEquals(expected.getToIndex(), actual.getToIndex()); + Assert.assertEquals(expected.getType(), actual.getType()); + } + + private RuleChainMetaData createRuleChainMetaData(RuleChainId ruleChainId, Integer firstNodeIndex, List nodes, List connections) { + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(ruleChainId); + ruleChainMetaData.setFirstNodeIndex(firstNodeIndex); + ruleChainMetaData.setNodes(nodes); + ruleChainMetaData.setConnections(connections); + ruleChainMetaData.setRuleChainConnections(null); + return ruleChainMetaData; + } + + private List createConnections() { + List result = new ArrayList<>(); + result.add(createNodeConnectionInfo(3, 6, "Success")); + result.add(createNodeConnectionInfo(3, 10, "Success")); + result.add(createNodeConnectionInfo(3, 0, "Success")); + result.add(createNodeConnectionInfo(4, 11, "Success")); + result.add(createNodeConnectionInfo(5, 11, "Success")); + result.add(createNodeConnectionInfo(6, 11, "Attributes Updated")); + result.add(createNodeConnectionInfo(6, 7, "RPC Request from Device")); + result.add(createNodeConnectionInfo(6, 4, "Post telemetry")); + result.add(createNodeConnectionInfo(6, 5, "Post attributes")); + result.add(createNodeConnectionInfo(6, 8, "Other")); + result.add(createNodeConnectionInfo(6, 9, "RPC Request to Device")); + result.add(createNodeConnectionInfo(7, 11, "Success")); + result.add(createNodeConnectionInfo(10, 9, "RPC")); + return result; + } + + private NodeConnectionInfo createNodeConnectionInfo(int fromIndex, int toIndex, String type) { + NodeConnectionInfo result = new NodeConnectionInfo(); + result.setFromIndex(fromIndex); + result.setToIndex(toIndex); + result.setType(type); + return result; + } + + private List createRuleNodes(RuleChainId ruleChainId) throws JsonProcessingException { + List result = new ArrayList<>(); + result.add(getOutputNode(ruleChainId)); + result.add(getAcknowledgeNode(ruleChainId)); + result.add(getCheckpointNode(ruleChainId)); + result.add(getDeviceProfileNode(ruleChainId)); + result.add(getSaveTimeSeriesNode(ruleChainId)); + result.add(getSaveClientAttributesNode(ruleChainId)); + result.add(getMessageTypeSwitchNode(ruleChainId)); + result.add(getLogRpcFromDeviceNode(ruleChainId)); + result.add(getLogOtherNode(ruleChainId)); + result.add(getRpcCallRequestNode(ruleChainId)); + result.add(getPushToAnalyticsNode(ruleChainId)); + result.add(getPushToCloudNode(ruleChainId)); + return result; + } + + private RuleNode createRuleNode(RuleChainId ruleChainId, String type, String name, JsonNode configuration, JsonNode additionalInfo) { + RuleNode e = new RuleNode(); + e.setRuleChainId(ruleChainId); + e.setType(type); + e.setName(name); + e.setDebugMode(false); + e.setConfiguration(configuration); + e.setAdditionalInfo(additionalInfo); + e.setId(new RuleNodeId(UUID.randomUUID())); + return e; + } + + private List createConnectionsInDifferentOrder() { + List result = new ArrayList<>(); + result.add(createNodeConnectionInfo(0, 2, "RPC")); + result.add(createNodeConnectionInfo(4, 1, "Success")); + result.add(createNodeConnectionInfo(5, 1, "Attributes Updated")); + result.add(createNodeConnectionInfo(5, 4, "RPC Request from Device")); + result.add(createNodeConnectionInfo(5, 7, "Post telemetry")); + result.add(createNodeConnectionInfo(5, 6, "Post attributes")); + result.add(createNodeConnectionInfo(5, 3, "Other")); + result.add(createNodeConnectionInfo(5, 2, "RPC Request to Device")); + result.add(createNodeConnectionInfo(6, 1, "Success")); + result.add(createNodeConnectionInfo(7, 1, "Success")); + result.add(createNodeConnectionInfo(8, 11, "Success")); + result.add(createNodeConnectionInfo(8, 5, "Success")); + result.add(createNodeConnectionInfo(8, 0, "Success")); + return result; + } + + private List createRuleNodesInDifferentOrder(RuleChainId ruleChainId) throws JsonProcessingException { + List result = new ArrayList<>(); + result.add(getPushToAnalyticsNode(ruleChainId)); + result.add(getPushToCloudNode(ruleChainId)); + result.add(getRpcCallRequestNode(ruleChainId)); + result.add(getLogOtherNode(ruleChainId)); + result.add(getLogRpcFromDeviceNode(ruleChainId)); + result.add(getMessageTypeSwitchNode(ruleChainId)); + result.add(getSaveClientAttributesNode(ruleChainId)); + result.add(getSaveTimeSeriesNode(ruleChainId)); + result.add(getDeviceProfileNode(ruleChainId)); + result.add(getCheckpointNode(ruleChainId)); + result.add(getAcknowledgeNode(ruleChainId)); + result.add(getOutputNode(ruleChainId)); + return result; + } + + + @NotNull + private RuleNode getOutputNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.flow.TbRuleChainOutputNode", + "Output node", + mapper.readTree("{\"version\":0}"), + mapper.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":592}")); + } + + @NotNull + private RuleNode getCheckpointNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.flow.TbCheckpointNode", + "Checkpoint node", + mapper.readTree("{\"queueName\":\"HighPriority\"}"), + mapper.readTree("{\"description\":\"\",\"layoutX\":178,\"layoutY\":647}")); + } + + @NotNull + private RuleNode getSaveTimeSeriesNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", + "Save Timeseries", + mapper.readTree("{\"defaultTTL\":0}"), + mapper.readTree("{\"layoutX\":823,\"layoutY\":157}")); + } + + @NotNull + private RuleNode getMessageTypeSwitchNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", + "Message Type Switch", + mapper.readTree("{\"version\":0}"), + mapper.readTree("{\"layoutX\":347,\"layoutY\":149}")); + } + + @NotNull + private RuleNode getLogOtherNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.action.TbLogNode", + "Log Other", + mapper.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), + mapper.readTree("{\"layoutX\":824,\"layoutY\":378}")); + } + + @NotNull + private RuleNode getPushToCloudNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", + "Push to cloud", + mapper.readTree("{\"scope\":\"SERVER_SCOPE\"}"), + mapper.readTree("{\"layoutX\":1129,\"layoutY\":52}")); + } + + @NotNull + private RuleNode getAcknowledgeNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.flow.TbAckNode", + "Acknowledge node", + mapper.readTree("{\"version\":0}"), + mapper.readTree("{\"description\":\"\",\"layoutX\":177,\"layoutY\":703}")); + } + + @NotNull + private RuleNode getDeviceProfileNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "Device Profile Node", + mapper.readTree("{\"persistAlarmRulesState\":false,\"fetchAlarmRulesStateOnStart\":false}"), + mapper.readTree("{\"description\":\"Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \\\"Success\\\" relation type.\",\"layoutX\":187,\"layoutY\":468}")); + } + + @NotNull + private RuleNode getSaveClientAttributesNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", + "Save Client Attributes", + mapper.readTree("{\"scope\":\"CLIENT_SCOPE\"}"), + mapper.readTree("{\"layoutX\":824,\"layoutY\":52}")); + } + + @NotNull + private RuleNode getLogRpcFromDeviceNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.action.TbLogNode", + "Log RPC from Device", + mapper.readTree("{\"jsScript\":\"return '\\\\nIncoming message:\\\\n' + JSON.stringify(msg) + '\\\\nIncoming metadata:\\\\n' + JSON.stringify(metadata);\"}"), + mapper.readTree("{\"layoutX\":825,\"layoutY\":266}")); + } + + @NotNull + private RuleNode getRpcCallRequestNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", + "RPC Call Request", + mapper.readTree("{\"timeoutInSeconds\":60}"), + mapper.readTree("{\"layoutX\":824,\"layoutY\":466}")); + } + + @NotNull + private RuleNode getPushToAnalyticsNode(RuleChainId ruleChainId) throws JsonProcessingException { + return createRuleNode(ruleChainId, + "org.thingsboard.rule.engine.flow.TbRuleChainInputNode", + "Push to Analytics", + mapper.readTree("{\"ruleChainId\":\"af588000-6c7c-11ec-bafd-c9a47a5c8d99\"}"), + mapper.readTree("{\"description\":\"\",\"layoutX\":477,\"layoutY\":560}")); + } +} \ No newline at end of file diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 8586d9b920..e54b5d5765 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -30,6 +30,11 @@ service EdgeRpcService { } +enum EdgeVersion { + V_3_3_0 = 0; + V_3_3_3 = 1; +} + /** * Data Structures; */ @@ -61,6 +66,7 @@ message EdgeUpdateMsg { message ConnectRequestMsg { string edgeRoutingKey = 1; string edgeSecret = 2; + EdgeVersion edgeVersion = 3; } enum ConnectResponseCode { From f115f3ac00bb0826ce60a2d812af80cd59014e7b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 12 Jan 2022 15:53:16 +0200 Subject: [PATCH 088/403] Added edge version to connect msg --- .../main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index ec6670f807..f1f36ab4a4 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkResponseMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.RequestMsgType; import org.thingsboard.server.gen.edge.v1.ResponseMsg; @@ -97,7 +98,11 @@ public class EdgeGrpcClient implements EdgeRpcClient { this.inputStream = stub.withCompression("gzip").handleMsgs(initOutputStream(edgeKey, onUplinkResponse, onEdgeUpdate, onDownlink, onError)); this.inputStream.onNext(RequestMsg.newBuilder() .setMsgType(RequestMsgType.CONNECT_RPC_MESSAGE) - .setConnectRequestMsg(ConnectRequestMsg.newBuilder().setEdgeRoutingKey(edgeKey).setEdgeSecret(edgeSecret).build()) + .setConnectRequestMsg(ConnectRequestMsg.newBuilder() + .setEdgeRoutingKey(edgeKey) + .setEdgeSecret(edgeSecret) + .setEdgeVersion(EdgeVersion.V_3_3_3) + .build()) .build()); } From 926642bf8a5eb32a61d412adb833c8fddbfe37e9 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 12 Jan 2022 16:20:46 +0200 Subject: [PATCH 089/403] fixed column name in upgrade query --- .../server/service/install/SqlDatabaseUpgradeService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4a6d64caea..51741fad5b 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 @@ -476,9 +476,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); try { - conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" + " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;"); - conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" + " select a.tenant_id, r.from_id, created_time, type, customer_id, id" + " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;"); conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';"); From 7e71e909e39f669ea612dc9d4763fcd767c013a5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Jan 2022 17:31:30 +0200 Subject: [PATCH 090/403] UI: Rename variable persistentPageLinkMode --- .../app/modules/home/components/alarm/alarm-table-config.ts | 2 +- .../home/components/audit-log/audit-log-table-config.ts | 4 ++-- .../home/components/audit-log/audit-log-table.component.ts | 4 ++-- .../home/components/entity/entities-table.component.ts | 6 +++--- .../app/modules/home/components/event/event-table-config.ts | 2 +- .../home/models/entity/entities-table-config.models.ts | 2 +- .../home/models/entity/entity-table-component.models.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 6c4e0a22e0..9a200b8a3d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -60,7 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; - this.persistentPageLinkMode = false; + this.pageMode = false; this.defaultTimewindowInterval = historyInterval(DAY * 30); this.detailsPanelEnabled = false; this.selectionEnabled = false; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index a61d0c54ab..2a5d339230 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -53,12 +53,12 @@ export class AuditLogTableConfig extends EntityTableConfig>; @@ -206,7 +206,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.displayPagination = this.entitiesTableConfig.displayPagination; this.defaultPageSize = this.entitiesTableConfig.defaultPageSize; - this.persistentPageLinkMode = this.entitiesTableConfig.persistentPageLinkMode; + this.pageMode = this.entitiesTableConfig.pageMode; this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; if (this.entitiesTableConfig.useTimePageLink) { @@ -640,7 +640,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { - if (this.persistentPageLinkMode) { + if (this.pageMode) { this.router.navigate([], { relativeTo: this.route, queryParams, diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index d6eedb7ff6..a10200060a 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -92,7 +92,7 @@ export class EventTableConfig extends EntityTableConfig { this.searchEnabled = false; this.addEnabled = false; this.entitiesDeleteEnabled = false; - this.persistentPageLinkMode = false; + this.pageMode = false; this.headerComponent = EventTableHeaderComponent; diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index d6ddf40735..d5866cbd74 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -160,7 +160,7 @@ export class EntityTableConfig, P extends PageLink = P addDialogStyle = {}; defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC}; displayPagination = true; - persistentPageLinkMode = true; + pageMode = true; defaultPageSize = 10; columns: Array> = []; cellActionDescriptors: Array> = []; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 4cf41f0fed..3c1a7428ab 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -50,7 +50,7 @@ export interface IEntitiesTableComponent { displayPagination: boolean; pageSizeOptions: number[]; pageLink: PageLink; - persistentPageLinkMode: boolean; + pageMode: boolean; textSearchMode: boolean; timewindow: Timewindow; dataSource: EntitiesDataSource>; From 104357b7b9afcdb6b7337a589e436465bcdc8bc4 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 12 Jan 2022 17:42:12 +0200 Subject: [PATCH 091/403] Use widgettypedetails instead of widgettype in edge processor --- .../constructor/WidgetTypeMsgConstructor.java | 32 +++++++++++-------- .../processor/WidgetTypeEdgeProcessor.java | 8 ++--- common/edge-api/src/main/proto/edge.proto | 2 ++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java index ed911297f7..6f45f46327 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -28,26 +28,32 @@ import org.thingsboard.server.queue.util.TbCoreComponent; @TbCoreComponent public class WidgetTypeMsgConstructor { - public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetType widgetType) { + public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) { WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder() .setMsgType(msgType) - .setIdMSB(widgetType.getId().getId().getMostSignificantBits()) - .setIdLSB(widgetType.getId().getId().getLeastSignificantBits()); - if (widgetType.getBundleAlias() != null) { - builder.setBundleAlias(widgetType.getBundleAlias()); + .setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits()) + .setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()); + if (widgetTypeDetails.getBundleAlias() != null) { + builder.setBundleAlias(widgetTypeDetails.getBundleAlias()); } - if (widgetType.getAlias() != null) { - builder.setAlias(widgetType.getAlias()); + if (widgetTypeDetails.getAlias() != null) { + builder.setAlias(widgetTypeDetails.getAlias()); } - if (widgetType.getName() != null) { - builder.setName(widgetType.getName()); + if (widgetTypeDetails.getName() != null) { + builder.setName(widgetTypeDetails.getName()); } - if (widgetType.getDescriptor() != null) { - builder.setDescriptorJson(JacksonUtil.toString(widgetType.getDescriptor())); + if (widgetTypeDetails.getDescriptor() != null) { + builder.setDescriptorJson(JacksonUtil.toString(widgetTypeDetails.getDescriptor())); } - if (widgetType.getTenantId().equals(TenantId.SYS_TENANT_ID)) { + if (widgetTypeDetails.getTenantId().equals(TenantId.SYS_TENANT_ID)) { builder.setIsSystem(true); } + if (widgetTypeDetails.getImage() != null) { + builder.setImage(widgetTypeDetails.getImage()); + } + if (widgetTypeDetails.getDescription() != null) { + builder.setDescription(widgetTypeDetails.getDescription()); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java index de4e0e9a06..6e481de21b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/WidgetTypeEdgeProcessor.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.WidgetTypeId; -import org.thingsboard.server.common.data.widget.WidgetType; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; @@ -38,10 +38,10 @@ public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { switch (edgeEdgeEventActionType) { case ADDED: case UPDATED: - WidgetType widgetType = widgetTypeService.findWidgetTypeById(edgeEvent.getTenantId(), widgetTypeId); - if (widgetType != null) { + WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(edgeEvent.getTenantId(), widgetTypeId); + if (widgetTypeDetails != null) { WidgetTypeUpdateMsg widgetTypeUpdateMsg = - widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetType); + widgetTypeMsgConstructor.constructWidgetTypeUpdateMsg(msgType, widgetTypeDetails); downlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addWidgetTypeUpdateMsg(widgetTypeUpdateMsg) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index e54b5d5765..3d4cdc7fe8 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -333,6 +333,8 @@ message WidgetTypeUpdateMsg { optional string name = 6; optional string descriptorJson = 7; bool isSystem = 8; + string image = 9; + string description = 10; } message AdminSettingsUpdateMsg { From 7820cb22de4c9cfb1d1b19b36945d69531428eb7 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 12 Jan 2022 17:51:27 +0200 Subject: [PATCH 092/403] lwm2m: preparing for certificate validation in bootstrap mode --- .../secure/TbLwM2MCertificateVerifier.java | 91 +++++++++++++++++++ .../TbLwM2MDtlsCertificateVerifier.java | 54 +---------- 2 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java new file mode 100644 index 0000000000..620d7243c4 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; + +@Slf4j +@Component +@TbLwM2mTransportComponent +@RequiredArgsConstructor +public class TbLwM2MCertificateVerifier { + + private final LwM2MTransportServerConfig config; + private final LwM2mCredentialsSecurityInfoValidator securityInfoValidator; + + public TbLwM2MSecurityInfo verifyCertificate(X509Certificate cert, String sha3Hash, LwM2mTypeServer lwM2mTypeServer) { + TbLwM2MSecurityInfo securityInfo = null; + // verify if trust + if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { + if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { + String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); + securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, lwM2mTypeServer) : null; + } + } + // if not trust or cert trust securityInfo == null + if (securityInfo == null) { + try { + securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, lwM2mTypeServer); + } catch (LwM2MAuthException e) { + log.trace("Failed find security info: {}", sha3Hash, e); + } + } + return securityInfo; + } + + private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); + for (int index = 0; index < certificates.length; ++index) { + X509Certificate caCert = certificates[index]; + try { + TrustAnchor trustAnchor = new TrustAnchor(caCert, null); + CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + PKIXParameters pkixParams = new PKIXParameters( + Collections.singleton(trustAnchor)); + pkixParams.setRevocationEnabled(false); + if (cpv.validate(cp, pkixParams) != null) return certificate; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { + log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); + } + } + } catch (CertificateException e) { + log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); + } + return null; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java index babf385bc8..5703f5b366 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -29,7 +29,6 @@ import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; import org.eclipse.californium.scandium.dtls.x509.StaticCertificateVerifier; import org.eclipse.californium.scandium.util.ServerNames; -import org.eclipse.leshan.core.util.SecurityUtil; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -44,28 +43,18 @@ import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MClientCredentials; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; import javax.annotation.PostConstruct; import javax.security.auth.x500.X500Principal; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.Collections; import java.util.List; import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.CLIENT; @@ -80,6 +69,7 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator securityInfoValidator; private final TbMainSecurityStore securityStore; + private final TbLwM2MCertificateVerifier certificateVerifier; @SuppressWarnings("deprecation") private StaticCertificateVerifier staticCertificateVerifier; @@ -124,26 +114,9 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer if (!skipValidityCheckForClientCert) { cert.checkValidity(); } - - - TbLwM2MSecurityInfo securityInfo = null; - // verify if trust - if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { - if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { - String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); - securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, CLIENT) : null; - } - } - // if not trust or cert trust securityInfo == null String strCert = SslUtil.getCertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - if (securityInfo == null) { - try { - securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, CLIENT); - } catch (LwM2MAuthException e) { - log.trace("Failed find security info: {}", sha3Hash, e); - } - } + TbLwM2MSecurityInfo securityInfo = certificateVerifier.verifyCertificate(cert, sha3Hash, CLIENT); ValidateDeviceCredentialsResponse msg = securityInfo != null ? securityInfo.getMsg() : null; if (msg != null && org.thingsboard.server.common.data.StringUtils.isNotEmpty(msg.getCredentials())) { LwM2MClientCredentials credentials = JacksonUtil.fromString(msg.getCredentials(), LwM2MClientCredentials.class); @@ -201,27 +174,4 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer public void setResultHandler(HandshakeResultHandler resultHandler) { } - - private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); - for (int index = 0; index < certificates.length; ++index) { - X509Certificate caCert = certificates[index]; - try { - TrustAnchor trustAnchor = new TrustAnchor(caCert, null); - CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); - PKIXParameters pkixParams = new PKIXParameters( - Collections.singleton(trustAnchor)); - pkixParams.setRevocationEnabled(false); - if (cpv.validate(cp, pkixParams) != null) return certificate; - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { - log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); - } - } - } catch (CertificateException e) { - log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); - } - return null; - } } From c3c23e079680669f3068b036eec9d65167a311e0 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 12 Jan 2022 17:54:28 +0200 Subject: [PATCH 093/403] Added optional for widgettype image and description --- common/edge-api/src/main/proto/edge.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 3d4cdc7fe8..243a77bd6b 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -333,8 +333,8 @@ message WidgetTypeUpdateMsg { optional string name = 6; optional string descriptorJson = 7; bool isSystem = 8; - string image = 9; - string description = 10; + optional string image = 9; + optional string description = 10; } message AdminSettingsUpdateMsg { From b4a1c0f83607c723fce2451c5d7a6212f89d33eb Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 12 Jan 2022 19:01:11 +0200 Subject: [PATCH 094/403] Increased timeout for timeseries with failures test --- .../src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java index c412e86ec6..1a3e4eef0d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -1137,7 +1137,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { clusterService.onEdgeEventUpdate(tenantId, edge.getId()); } - Assert.assertTrue(edgeImitator.waitForMessages(60)); + Assert.assertTrue(edgeImitator.waitForMessages(120)); List allTelemetryMsgs = edgeImitator.findAllMessagesByType(EntityDataProto.class); Assert.assertEquals(numberOfTimeseriesToSend, allTelemetryMsgs.size()); From 5bf75acccedca62b2f628d7a95ce203822c51a68 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 12 Jan 2022 19:01:47 +0200 Subject: [PATCH 095/403] Update material icons font --- ui-ngx/angular.json | 5 - ui-ngx/package.json | 1 - .../services/material-icons-codepoints.raw | 2142 +++++++++++++++++ ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../assets/fonts/MaterialIcons-Regular.ttf | Bin 0 -> 337868 bytes ui-ngx/src/assets/fonts/material-icons.css | 33 + ui-ngx/src/index.html | 2 +- ui-ngx/yarn.lock | 5 - 8 files changed, 2177 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/core/services/material-icons-codepoints.raw create mode 100644 ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf create mode 100644 ui-ngx/src/assets/fonts/material-icons.css diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 7565ae770c..0691b25b4a 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -63,11 +63,6 @@ "glob": "marker-shadow.png", "input": "node_modules/leaflet/dist/images/", "output": "/" - }, - { - "glob": "**/*", - "input": "node_modules/material-design-icons/iconfont/", - "output": "assets/fonts" } ], "styles": [ diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 1153524a26..d3174bfc4d 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -61,7 +61,6 @@ "leaflet-providers": "^1.13.0", "leaflet.gridlayer.googlemutant": "^0.13.4", "leaflet.markercluster": "^1.5.3", - "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", "moment": "^2.29.1", "moment-timezone": "^0.5.34", diff --git a/ui-ngx/src/app/core/services/material-icons-codepoints.raw b/ui-ngx/src/app/core/services/material-icons-codepoints.raw new file mode 100644 index 0000000000..a1f9b8bbc7 --- /dev/null +++ b/ui-ngx/src/app/core/services/material-icons-codepoints.raw @@ -0,0 +1,2142 @@ +10k e951 +10mp e952 +11mp e953 +123 eb8d +12mp e954 +13mp e955 +14mp e956 +15mp e957 +16mp e958 +17mp e959 +18mp e95a +19mp e95b +1k e95c +1k_plus e95d +1x_mobiledata efcd +20mp e95e +21mp e95f +22mp e960 +23mp e961 +24mp e962 +2k e963 +2k_plus e964 +2mp e965 +30fps efce +30fps_select efcf +360 e577 +3d_rotation e84d +3g_mobiledata efd0 +3k e966 +3k_plus e967 +3mp e968 +3p efd1 +4g_mobiledata efd2 +4g_plus_mobiledata efd3 +4k e072 +4k_plus e969 +4mp e96a +5g ef38 +5k e96b +5k_plus e96c +5mp e96d +60fps efd4 +60fps_select efd5 +6_ft_apart f21e +6k e96e +6k_plus e96f +6mp e970 +7k e971 +7k_plus e972 +7mp e973 +8k e974 +8k_plus e975 +8mp e976 +9k e977 +9k_plus e978 +9mp e979 +abc eb94 +ac_unit eb3b +access_alarm e190 +access_alarms e191 +access_time e192 +access_time_filled efd6 +accessibility e84e +accessibility_new e92c +accessible e914 +accessible_forward e934 +account_balance e84f +account_balance_wallet e850 +account_box e851 +account_circle e853 +account_tree e97a +ad_units ef39 +adb e60e +add e145 +add_a_photo e439 +add_alarm e193 +add_alert e003 +add_box e146 +add_business e729 +add_call e0e8 +add_card eb86 +add_chart e97b +add_circle e147 +add_circle_outline e148 +add_comment e266 +add_ic_call e97c +add_link e178 +add_location e567 +add_location_alt ef3a +add_moderator e97d +add_photo_alternate e43e +add_reaction e1d3 +add_road ef3b +add_shopping_cart e854 +add_task f23a +add_to_drive e65c +add_to_home_screen e1fe +add_to_photos e39d +add_to_queue e05c +addchart ef3c +adf_scanner eada +adjust e39e +admin_panel_settings ef3d +adobe ea96 +ads_click e762 +agriculture ea79 +air efd8 +airline_seat_flat e630 +airline_seat_flat_angled e631 +airline_seat_individual_suite e632 +airline_seat_legroom_extra e633 +airline_seat_legroom_normal e634 +airline_seat_legroom_reduced e635 +airline_seat_recline_extra e636 +airline_seat_recline_normal e637 +airline_stops e7d0 +airlines e7ca +airplane_ticket efd9 +airplanemode_active e195 +airplanemode_inactive e194 +airplanemode_off e194 +airplanemode_on e195 +airplay e055 +airport_shuttle eb3c +alarm e855 +alarm_add e856 +alarm_off e857 +alarm_on e858 +album e019 +align_horizontal_center e00f +align_horizontal_left e00d +align_horizontal_right e010 +align_vertical_bottom e015 +align_vertical_center e011 +align_vertical_top e00c +all_inbox e97f +all_inclusive eb3d +all_out e90b +alt_route f184 +alternate_email e0e6 +amp_stories ea13 +analytics ef3e +anchor f1cd +android e859 +animation e71c +announcement e85a +aod efda +apartment ea40 +api f1b7 +app_blocking ef3f +app_registration ef40 +app_settings_alt ef41 +app_shortcut eae4 +apple ea80 +approval e982 +apps e5c3 +apps_outage e7cc +architecture ea3b +archive e149 +area_chart e770 +arrow_back e5c4 +arrow_back_ios e5e0 +arrow_back_ios_new e2ea +arrow_circle_down f181 +arrow_circle_left eaa7 +arrow_circle_right eaaa +arrow_circle_up f182 +arrow_downward e5db +arrow_drop_down e5c5 +arrow_drop_down_circle e5c6 +arrow_drop_up e5c7 +arrow_forward e5c8 +arrow_forward_ios e5e1 +arrow_left e5de +arrow_right e5df +arrow_right_alt e941 +arrow_upward e5d8 +art_track e060 +article ef42 +aspect_ratio e85b +assessment e85c +assignment e85d +assignment_ind e85e +assignment_late e85f +assignment_return e860 +assignment_returned e861 +assignment_turned_in e862 +assistant e39f +assistant_direction e988 +assistant_navigation e989 +assistant_photo e3a0 +assured_workload eb6f +atm e573 +attach_email ea5e +attach_file e226 +attach_money e227 +attachment e2bc +attractions ea52 +attribution efdb +audio_file eb82 +audiotrack e3a1 +auto_awesome e65f +auto_awesome_mosaic e660 +auto_awesome_motion e661 +auto_delete ea4c +auto_fix_high e663 +auto_fix_normal e664 +auto_fix_off e665 +auto_graph e4fb +auto_stories e666 +autofps_select efdc +autorenew e863 +av_timer e01b +baby_changing_station f19b +back_hand e764 +backpack f19c +backspace e14a +backup e864 +backup_table ef43 +badge ea67 +bakery_dining ea53 +balance eaf6 +balcony e58f +ballot e172 +bar_chart e26b +batch_prediction f0f5 +bathroom efdd +bathtub ea41 +battery_0_bar ebdc +battery_1_bar ebd9 +battery_2_bar ebe0 +battery_3_bar ebdd +battery_4_bar ebe2 +battery_5_bar ebd4 +battery_6_bar ebd2 +battery_alert e19c +battery_charging_full e1a3 +battery_full e1a4 +battery_saver efde +battery_std e1a5 +battery_unknown e1a6 +beach_access eb3e +bed efdf +bedroom_baby efe0 +bedroom_child efe1 +bedroom_parent efe2 +bedtime ef44 +bedtime_off eb76 +beenhere e52d +bento f1f4 +bike_scooter ef45 +biotech ea3a +blender efe3 +block e14b +block_flipped ef46 +bloodtype efe4 +bluetooth e1a7 +bluetooth_audio e60f +bluetooth_connected e1a8 +bluetooth_disabled e1a9 +bluetooth_drive efe5 +bluetooth_searching e1aa +blur_circular e3a2 +blur_linear e3a3 +blur_off e3a4 +blur_on e3a5 +bolt ea0b +book e865 +book_online f217 +bookmark e866 +bookmark_add e598 +bookmark_added e599 +bookmark_border e867 +bookmark_outline e867 +bookmark_remove e59a +bookmarks e98b +border_all e228 +border_bottom e229 +border_clear e22a +border_color e22b +border_horizontal e22c +border_inner e22d +border_left e22e +border_outer e22f +border_right e230 +border_style e231 +border_top e232 +border_vertical e233 +boy eb67 +branding_watermark e06b +breakfast_dining ea54 +brightness_1 e3a6 +brightness_2 e3a7 +brightness_3 e3a8 +brightness_4 e3a9 +brightness_5 e3aa +brightness_6 e3ab +brightness_7 e3ac +brightness_auto e1ab +brightness_high e1ac +brightness_low e1ad +brightness_medium e1ae +broken_image e3ad +browse_gallery ebd1 +browser_not_supported ef47 +browser_updated e7cf +brunch_dining ea73 +brush e3ae +bubble_chart e6dd +bug_report e868 +build e869 +build_circle ef48 +bungalow e591 +burst_mode e43c +bus_alert e98f +business e0af +business_center eb3f +cabin e589 +cable efe6 +cached e86a +cake e7e9 +calculate ea5f +calendar_month ebcc +calendar_today e935 +calendar_view_day e936 +calendar_view_month efe7 +calendar_view_week efe8 +call e0b0 +call_end e0b1 +call_made e0b2 +call_merge e0b3 +call_missed e0b4 +call_missed_outgoing e0e4 +call_received e0b5 +call_split e0b6 +call_to_action e06c +camera e3af +camera_alt e3b0 +camera_enhance e8fc +camera_front e3b1 +camera_indoor efe9 +camera_outdoor efea +camera_rear e3b2 +camera_roll e3b3 +cameraswitch efeb +campaign ef49 +cancel e5c9 +cancel_presentation e0e9 +cancel_schedule_send ea39 +candlestick_chart ead4 +car_crash ebf2 +car_rental ea55 +car_repair ea56 +card_giftcard e8f6 +card_membership e8f7 +card_travel e8f8 +carpenter f1f8 +cases e992 +casino eb40 +cast e307 +cast_connected e308 +cast_for_education efec +castle eab1 +catching_pokemon e508 +category e574 +celebration ea65 +cell_tower ebba +cell_wifi e0ec +center_focus_strong e3b4 +center_focus_weak e3b5 +chair efed +chair_alt efee +chalet e585 +change_circle e2e7 +change_history e86b +charging_station f19d +chat e0b7 +chat_bubble e0ca +chat_bubble_outline e0cb +check e5ca +check_box e834 +check_box_outline_blank e835 +check_circle e86c +check_circle_outline e92d +checklist e6b1 +checklist_rtl e6b3 +checkroom f19e +chevron_left e5cb +chevron_right e5cc +child_care eb41 +child_friendly eb42 +chrome_reader_mode e86d +church eaae +circle ef4a +circle_notifications e994 +class e86e +clean_hands f21f +cleaning_services f0ff +clear e14c +clear_all e0b8 +close e5cd +close_fullscreen f1cf +closed_caption e01c +closed_caption_disabled f1dc +closed_caption_off e996 +cloud e2bd +cloud_circle e2be +cloud_done e2bf +cloud_download e2c0 +cloud_off e2c1 +cloud_queue e2c2 +cloud_sync eb5a +cloud_upload e2c3 +cloudy_snowing e810 +co2 e7b0 +co_present eaf0 +code e86f +code_off e4f3 +coffee efef +coffee_maker eff0 +collections e3b6 +collections_bookmark e431 +color_lens e3b7 +colorize e3b8 +comment e0b9 +comment_bank ea4e +comments_disabled e7a2 +commit eaf5 +commute e940 +compare e3b9 +compare_arrows e915 +compass_calibration e57c +compost e761 +compress e94d +computer e30a +confirmation_num e638 +confirmation_number e638 +connect_without_contact f223 +connected_tv e998 +connecting_airports e7c9 +construction ea3c +contact_mail e0d0 +contact_page f22e +contact_phone e0cf +contact_support e94c +contactless ea71 +contacts e0ba +content_copy e14d +content_cut e14e +content_paste e14f +content_paste_go ea8e +content_paste_off e4f8 +content_paste_search ea9b +contrast eb37 +control_camera e074 +control_point e3ba +control_point_duplicate e3bb +cookie eaac +copy_all e2ec +copyright e90c +coronavirus f221 +corporate_fare f1d0 +cottage e587 +countertops f1f7 +create e150 +create_new_folder e2cc +credit_card e870 +credit_card_off e4f4 +credit_score eff1 +crib e588 +crisis_alert ebe9 +crop e3be +crop_16_9 e3bc +crop_3_2 e3bd +crop_5_4 e3bf +crop_7_5 e3c0 +crop_din e3c1 +crop_free e3c2 +crop_landscape e3c3 +crop_original e3c4 +crop_portrait e3c5 +crop_rotate e437 +crop_square e3c6 +cruelty_free e799 +css eb93 +currency_bitcoin ebc5 +currency_exchange eb70 +currency_franc eafa +currency_lira eaef +currency_pound eaf1 +currency_ruble eaec +currency_rupee eaf7 +currency_yen eafb +currency_yuan eaf9 +cyclone ebd5 +dangerous e99a +dark_mode e51c +dashboard e871 +dashboard_customize e99b +data_array ead1 +data_exploration e76f +data_object ead3 +data_saver_off eff2 +data_saver_on eff3 +data_thresholding eb9f +data_usage e1af +date_range e916 +deblur eb77 +deck ea42 +dehaze e3c7 +delete e872 +delete_forever e92b +delete_outline e92e +delete_sweep e16c +delivery_dining ea72 +density_large eba9 +density_medium eb9e +density_small eba8 +departure_board e576 +description e873 +deselect ebb6 +design_services f10a +desktop_access_disabled e99d +desktop_mac e30b +desktop_windows e30c +details e3c8 +developer_board e30d +developer_board_off e4ff +developer_mode e1b0 +device_hub e335 +device_thermostat e1ff +device_unknown e339 +devices e1b1 +devices_fold ebde +devices_other e337 +dialer_sip e0bb +dialpad e0bc +diamond ead5 +difference eb7d +dining eff4 +dinner_dining ea57 +directions e52e +directions_bike e52f +directions_boat e532 +directions_boat_filled eff5 +directions_bus e530 +directions_bus_filled eff6 +directions_car e531 +directions_car_filled eff7 +directions_ferry e532 +directions_off f10f +directions_railway e534 +directions_railway_filled eff8 +directions_run e566 +directions_subway e533 +directions_subway_filled eff9 +directions_train e534 +directions_transit e535 +directions_transit_filled effa +directions_walk e536 +dirty_lens ef4b +disabled_by_default f230 +disabled_visible e76e +disc_full e610 +discord ea6c +discount ebc9 +display_settings eb97 +dnd_forwardslash e611 +dns e875 +do_disturb f08c +do_disturb_alt f08d +do_disturb_off f08e +do_disturb_on f08f +do_not_disturb e612 +do_not_disturb_alt e611 +do_not_disturb_off e643 +do_not_disturb_on e644 +do_not_disturb_on_total_silence effb +do_not_step f19f +do_not_touch f1b0 +dock e30e +document_scanner e5fa +domain e7ee +domain_add eb62 +domain_disabled e0ef +domain_verification ef4c +done e876 +done_all e877 +done_outline e92f +donut_large e917 +donut_small e918 +door_back effc +door_front effd +door_sliding effe +doorbell efff +double_arrow ea50 +downhill_skiing e509 +download f090 +download_done f091 +download_for_offline f000 +downloading f001 +drafts e151 +drag_handle e25d +drag_indicator e945 +draw e746 +drive_eta e613 +drive_file_move e675 +drive_file_move_outline e9a1 +drive_file_move_rtl e76d +drive_file_rename_outline e9a2 +drive_folder_upload e9a3 +dry f1b3 +dry_cleaning ea58 +duo e9a5 +dvr e1b2 +dynamic_feed ea14 +dynamic_form f1bf +e_mobiledata f002 +earbuds f003 +earbuds_battery f004 +east f1df +eco ea35 +edgesensor_high f005 +edgesensor_low f006 +edit e3c9 +edit_attributes e578 +edit_calendar e742 +edit_location e568 +edit_location_alt e1c5 +edit_note e745 +edit_notifications e525 +edit_off e950 +edit_road ef4d +egg eacc +egg_alt eac8 +eject e8fb +elderly f21a +elderly_woman eb69 +electric_bike eb1b +electric_car eb1c +electric_moped eb1d +electric_rickshaw eb1e +electric_scooter eb1f +electrical_services f102 +elevator f1a0 +email e0be +emergency e1eb +emergency_recording ebf4 +emergency_share ebf6 +emoji_emotions ea22 +emoji_events ea23 +emoji_flags ea1a +emoji_food_beverage ea1b +emoji_nature ea1c +emoji_objects ea24 +emoji_people ea1d +emoji_symbols ea1e +emoji_transportation ea1f +engineering ea3d +enhance_photo_translate e8fc +enhanced_encryption e63f +equalizer e01d +error e000 +error_outline e001 +escalator f1a1 +escalator_warning f1ac +euro ea15 +euro_symbol e926 +ev_station e56d +event e878 +event_available e614 +event_busy e615 +event_note e616 +event_repeat eb7b +event_seat e903 +exit_to_app e879 +expand e94f +expand_circle_down e7cd +expand_less e5ce +expand_more e5cf +explicit e01e +explore e87a +explore_off e9a8 +exposure e3ca +exposure_minus_1 e3cb +exposure_minus_2 e3cc +exposure_neg_1 e3cb +exposure_neg_2 e3cc +exposure_plus_1 e3cd +exposure_plus_2 e3ce +exposure_zero e3cf +extension e87b +extension_off e4f5 +face e87c +face_retouching_natural ef4e +face_retouching_off f007 +facebook f234 +fact_check f0c5 +factory ebbc +family_restroom f1a2 +fast_forward e01f +fast_rewind e020 +fastfood e57a +favorite e87d +favorite_border e87e +favorite_outline e87e +fax ead8 +featured_play_list e06d +featured_video e06e +feed f009 +feedback e87f +female e590 +fence f1f6 +festival ea68 +fiber_dvr e05d +fiber_manual_record e061 +fiber_new e05e +fiber_pin e06a +fiber_smart_record e062 +file_copy e173 +file_download e2c4 +file_download_done e9aa +file_download_off e4fe +file_open eaf3 +file_present ea0e +file_upload e2c6 +filter e3d3 +filter_1 e3d0 +filter_2 e3d1 +filter_3 e3d2 +filter_4 e3d4 +filter_5 e3d5 +filter_6 e3d6 +filter_7 e3d7 +filter_8 e3d8 +filter_9 e3d9 +filter_9_plus e3da +filter_alt ef4f +filter_alt_off eb32 +filter_b_and_w e3db +filter_center_focus e3dc +filter_drama e3dd +filter_frames e3de +filter_hdr e3df +filter_list e152 +filter_list_alt e94e +filter_list_off eb57 +filter_none e3e0 +filter_tilt_shift e3e2 +filter_vintage e3e3 +find_in_page e880 +find_replace e881 +fingerprint e90d +fire_extinguisher f1d8 +fire_hydrant f1a3 +fireplace ea43 +first_page e5dc +fit_screen ea10 +fitbit e82b +fitness_center eb43 +flag e153 +flag_circle eaf8 +flaky ef50 +flare e3e4 +flash_auto e3e5 +flash_off e3e6 +flash_on e3e7 +flashlight_off f00a +flashlight_on f00b +flatware f00c +flight e539 +flight_class e7cb +flight_land e904 +flight_takeoff e905 +flip e3e8 +flip_camera_android ea37 +flip_camera_ios ea38 +flip_to_back e882 +flip_to_front e883 +flood ebe6 +flourescent f00d +flutter_dash e00b +fmd_bad f00e +fmd_good f00f +foggy e818 +folder e2c7 +folder_copy ebbd +folder_delete eb34 +folder_off eb83 +folder_open e2c8 +folder_shared e2c9 +folder_special e617 +folder_zip eb2c +follow_the_signs f222 +font_download e167 +font_download_off e4f9 +food_bank f1f2 +forest ea99 +fork_left eba0 +fork_right ebac +format_align_center e234 +format_align_justify e235 +format_align_left e236 +format_align_right e237 +format_bold e238 +format_clear e239 +format_color_fill e23a +format_color_reset e23b +format_color_text e23c +format_indent_decrease e23d +format_indent_increase e23e +format_italic e23f +format_line_spacing e240 +format_list_bulleted e241 +format_list_numbered e242 +format_list_numbered_rtl e267 +format_overline eb65 +format_paint e243 +format_quote e244 +format_shapes e25e +format_size e245 +format_strikethrough e246 +format_textdirection_l_to_r e247 +format_textdirection_r_to_l e248 +format_underline e249 +format_underlined e249 +fort eaad +forum e0bf +forward e154 +forward_10 e056 +forward_30 e057 +forward_5 e058 +forward_to_inbox f187 +foundation f200 +free_breakfast eb44 +free_cancellation e748 +front_hand e769 +fullscreen e5d0 +fullscreen_exit e5d1 +functions e24a +g_mobiledata f010 +g_translate e927 +gamepad e30f +games e021 +garage f011 +gavel e90e +generating_tokens e749 +gesture e155 +get_app e884 +gif e908 +gif_box e7a3 +girl eb68 +gite e58b +goat 10fffd +golf_course eb45 +gpp_bad f012 +gpp_good f013 +gpp_maybe f014 +gps_fixed e1b3 +gps_not_fixed e1b4 +gps_off e1b5 +grade e885 +gradient e3e9 +grading ea4f +grain e3ea +graphic_eq e1b8 +grass f205 +grid_3x3 f015 +grid_4x4 f016 +grid_goldenratio f017 +grid_off e3eb +grid_on e3ec +grid_view e9b0 +group e7ef +group_add e7f0 +group_off e747 +group_remove e7ad +group_work e886 +groups f233 +h_mobiledata f018 +h_plus_mobiledata f019 +hail e9b1 +handshake ebcb +handyman f10b +hardware ea59 +hd e052 +hdr_auto f01a +hdr_auto_select f01b +hdr_enhanced_select ef51 +hdr_off e3ed +hdr_off_select f01c +hdr_on e3ee +hdr_on_select f01d +hdr_plus f01e +hdr_strong e3f1 +hdr_weak e3f2 +headphones f01f +headphones_battery f020 +headset e310 +headset_mic e311 +headset_off e33a +healing e3f3 +health_and_safety e1d5 +hearing e023 +hearing_disabled f104 +heart_broken eac2 +height ea16 +help e887 +help_center f1c0 +help_outline e8fd +hevc f021 +hexagon eb39 +hide_image f022 +hide_source f023 +high_quality e024 +highlight e25f +highlight_alt ef52 +highlight_off e888 +highlight_remove e888 +hiking e50a +history e889 +history_edu ea3e +history_toggle_off f17d +hive eaa6 +hls eb8a +hls_off eb8c +holiday_village e58a +home e88a +home_filled e9b2 +home_max f024 +home_mini f025 +home_repair_service f100 +home_work ea09 +horizontal_distribute e014 +horizontal_rule f108 +horizontal_split e947 +hot_tub eb46 +hotel e53a +hotel_class e743 +hourglass_bottom ea5c +hourglass_disabled ef53 +hourglass_empty e88b +hourglass_full e88c +hourglass_top ea5b +house ea44 +house_siding f202 +houseboat e584 +how_to_reg e174 +how_to_vote e175 +html eb7e +http e902 +https e88d +hub e9f4 +hvac f10e +ice_skating e50b +icecream ea69 +image e3f4 +image_aspect_ratio e3f5 +image_not_supported f116 +image_search e43f +imagesearch_roller e9b4 +import_contacts e0e0 +import_export e0c3 +important_devices e912 +inbox e156 +incomplete_circle e79b +indeterminate_check_box e909 +info e88e +info_outline e88f +input e890 +insert_chart e24b +insert_chart_outlined e26a +insert_comment e24c +insert_drive_file e24d +insert_emoticon e24e +insert_invitation e24f +insert_link e250 +insert_page_break eaca +insert_photo e251 +insights f092 +install_desktop eb71 +install_mobile eb72 +integration_instructions ef54 +interests e7c8 +interpreter_mode e83b +inventory e179 +inventory_2 e1a1 +invert_colors e891 +invert_colors_off e0c4 +invert_colors_on e891 +ios_share e6b8 +iron e583 +iso e3f6 +javascript eb7c +join_full eaeb +join_inner eaf4 +join_left eaf2 +join_right eaea +kayaking e50c +kebab_dining e842 +key e73c +key_off eb84 +keyboard e312 +keyboard_alt f028 +keyboard_arrow_down e313 +keyboard_arrow_left e314 +keyboard_arrow_right e315 +keyboard_arrow_up e316 +keyboard_backspace e317 +keyboard_capslock e318 +keyboard_command eae0 +keyboard_command_key eae7 +keyboard_control e5d3 +keyboard_control_key eae6 +keyboard_double_arrow_down ead0 +keyboard_double_arrow_left eac3 +keyboard_double_arrow_right eac9 +keyboard_double_arrow_up eacf +keyboard_hide e31a +keyboard_option eadf +keyboard_option_key eae8 +keyboard_return e31b +keyboard_tab e31c +keyboard_voice e31d +king_bed ea45 +kitchen eb47 +kitesurfing e50d +label e892 +label_important e937 +label_important_outline e948 +label_off e9b6 +label_outline e893 +lan eb2f +landscape e3f7 +landslide ebd7 +language e894 +laptop e31e +laptop_chromebook e31f +laptop_mac e320 +laptop_windows e321 +last_page e5dd +launch e895 +layers e53b +layers_clear e53c +leaderboard f20c +leak_add e3f8 +leak_remove e3f9 +leave_bags_at_home f21b +legend_toggle f11b +lens e3fa +lens_blur f029 +library_add e02e +library_add_check e9b7 +library_books e02f +library_music e030 +light f02a +light_mode e518 +lightbulb e0f0 +lightbulb_outline e90f +line_axis ea9a +line_style e919 +line_weight e91a +linear_scale e260 +link e157 +link_off e16f +linked_camera e438 +liquor ea60 +list e896 +list_alt e0ee +live_help e0c6 +live_tv e639 +living f02b +local_activity e53f +local_airport e53d +local_atm e53e +local_attraction e53f +local_bar e540 +local_cafe e541 +local_car_wash e542 +local_convenience_store e543 +local_dining e556 +local_drink e544 +local_fire_department ef55 +local_florist e545 +local_gas_station e546 +local_grocery_store e547 +local_hospital e548 +local_hotel e549 +local_laundry_service e54a +local_library e54b +local_mall e54c +local_movies e54d +local_offer e54e +local_parking e54f +local_pharmacy e550 +local_phone e551 +local_pizza e552 +local_play e553 +local_police ef56 +local_post_office e554 +local_print_shop e555 +local_printshop e555 +local_restaurant e556 +local_see e557 +local_shipping e558 +local_taxi e559 +location_city e7f1 +location_disabled e1b6 +location_history e55a +location_off e0c7 +location_on e0c8 +location_pin f1db +location_searching e1b7 +lock e897 +lock_clock ef57 +lock_open e898 +lock_outline e899 +lock_reset eade +login ea77 +logo_dev ead6 +logout e9ba +looks e3fc +looks_3 e3fb +looks_4 e3fd +looks_5 e3fe +looks_6 e3ff +looks_one e400 +looks_two e401 +loop e028 +loupe e402 +low_priority e16d +loyalty e89a +lte_mobiledata f02c +lte_plus_mobiledata f02d +luggage f235 +lunch_dining ea61 +mail e158 +mail_outline e0e1 +male e58e +man e4eb +manage_accounts f02e +manage_history ebe7 +manage_search f02f +map e55b +maps_home_work f030 +maps_ugc ef58 +margin e9bb +mark_as_unread e9bc +mark_chat_read f18b +mark_chat_unread f189 +mark_email_read f18c +mark_email_unread f18a +mark_unread_chat_alt eb9d +markunread e159 +markunread_mailbox e89b +masks f218 +maximize e930 +media_bluetooth_off f031 +media_bluetooth_on f032 +mediation efa7 +medical_information ebed +medical_services f109 +medication f033 +medication_liquid ea87 +meeting_room eb4f +memory e322 +menu e5d2 +menu_book ea19 +menu_open e9bd +merge eb98 +merge_type e252 +message e0c9 +messenger e0ca +messenger_outline e0cb +mic e029 +mic_external_off ef59 +mic_external_on ef5a +mic_none e02a +mic_off e02b +microwave f204 +military_tech ea3f +minimize e931 +minor_crash ebf1 +miscellaneous_services f10c +missed_video_call e073 +mms e618 +mobile_friendly e200 +mobile_off e201 +mobile_screen_share e0e7 +mobiledata_off f034 +mode f097 +mode_comment e253 +mode_edit e254 +mode_edit_outline f035 +mode_night f036 +mode_of_travel e7ce +mode_standby f037 +model_training f0cf +monetization_on e263 +money e57d +money_off e25c +money_off_csred f038 +monitor ef5b +monitor_heart eaa2 +monitor_weight f039 +monochrome_photos e403 +mood e7f2 +mood_bad e7f3 +moped eb28 +more e619 +more_horiz e5d3 +more_time ea5d +more_vert e5d4 +mosque eab2 +motion_photos_auto f03a +motion_photos_off e9c0 +motion_photos_on e9c1 +motion_photos_pause f227 +motion_photos_paused e9c2 +motorcycle e91b +mouse e323 +move_down eb61 +move_to_inbox e168 +move_up eb64 +movie e02c +movie_creation e404 +movie_filter e43a +moving e501 +mp e9c3 +multiline_chart e6df +multiple_stop f1b9 +multitrack_audio e1b8 +museum ea36 +music_note e405 +music_off e440 +music_video e063 +my_library_add e02e +my_library_books e02f +my_library_music e030 +my_location e55c +nat ef5c +nature e406 +nature_people e407 +navigate_before e408 +navigate_next e409 +navigation e55d +near_me e569 +near_me_disabled f1ef +nearby_error f03b +nearby_off f03c +network_cell e1b9 +network_check e640 +network_locked e61a +network_ping ebca +network_wifi e1ba +network_wifi_1_bar ebe4 +network_wifi_2_bar ebd6 +network_wifi_3_bar ebe1 +new_label e609 +new_releases e031 +newspaper eb81 +next_plan ef5d +next_week e16a +nfc e1bb +night_shelter f1f1 +nightlife ea62 +nightlight f03d +nightlight_round ef5e +nights_stay ea46 +no_accounts f03e +no_backpack f237 +no_cell f1a4 +no_crash ebf0 +no_drinks f1a5 +no_encryption e641 +no_encryption_gmailerrorred f03f +no_flash f1a6 +no_food f1a7 +no_luggage f23b +no_meals f1d6 +no_meals_ouline f229 +no_meeting_room eb4e +no_photography f1a8 +no_sim e0cc +no_stroller f1af +no_transfer f1d5 +noise_aware ebec +noise_control_off ebf3 +nordic_walking e50e +north f1e0 +north_east f1e1 +north_west f1e2 +not_accessible f0fe +not_interested e033 +not_listed_location e575 +not_started f0d1 +note e06f +note_add e89c +note_alt f040 +notes e26c +notification_add e399 +notification_important e004 +notifications e7f4 +notifications_active e7f7 +notifications_none e7f5 +notifications_off e7f6 +notifications_on e7f7 +notifications_paused e7f8 +now_wallpaper e1bc +now_widgets e1bd +numbers eac7 +offline_bolt e932 +offline_pin e90a +offline_share e9c5 +ondemand_video e63a +online_prediction f0eb +opacity e91c +open_in_browser e89d +open_in_full f1ce +open_in_new e89e +open_in_new_off e4f6 +open_with e89f +other_houses e58c +outbond f228 +outbound e1ca +outbox ef5f +outdoor_grill ea47 +outgoing_mail f0d2 +outlet f1d4 +outlined_flag e16e +output ebbe +padding e9c8 +pages e7f9 +pageview e8a0 +paid f041 +palette e40a +pan_tool e925 +pan_tool_alt ebb9 +panorama e40b +panorama_fish_eye e40c +panorama_fisheye e40c +panorama_horizontal e40d +panorama_horizontal_select ef60 +panorama_photosphere e9c9 +panorama_photosphere_select e9ca +panorama_vertical e40e +panorama_vertical_select ef61 +panorama_wide_angle e40f +panorama_wide_angle_select ef62 +paragliding e50f +park ea63 +party_mode e7fa +password f042 +pattern f043 +pause e034 +pause_circle e1a2 +pause_circle_filled e035 +pause_circle_outline e036 +pause_presentation e0ea +payment e8a1 +payments ef63 +paypal ea8d +pedal_bike eb29 +pending ef64 +pending_actions f1bb +pentagon eb50 +people e7fb +people_alt ea21 +people_outline e7fc +percent eb58 +perm_camera_mic e8a2 +perm_contact_cal e8a3 +perm_contact_calendar e8a3 +perm_data_setting e8a4 +perm_device_info e8a5 +perm_device_information e8a5 +perm_identity e8a6 +perm_media e8a7 +perm_phone_msg e8a8 +perm_scan_wifi e8a9 +person e7fd +person_add e7fe +person_add_alt ea4d +person_add_alt_1 ef65 +person_add_disabled e9cb +person_off e510 +person_outline e7ff +person_pin e55a +person_pin_circle e56a +person_remove ef66 +person_remove_alt_1 ef67 +person_search f106 +personal_injury e6da +personal_video e63b +pest_control f0fa +pest_control_rodent f0fd +pets e91d +phishing ead7 +phone e0cd +phone_android e324 +phone_bluetooth_speaker e61b +phone_callback e649 +phone_disabled e9cc +phone_enabled e9cd +phone_forwarded e61c +phone_in_talk e61d +phone_iphone e325 +phone_locked e61e +phone_missed e61f +phone_paused e620 +phonelink e326 +phonelink_erase e0db +phonelink_lock e0dc +phonelink_off e327 +phonelink_ring e0dd +phonelink_setup e0de +photo e410 +photo_album e411 +photo_camera e412 +photo_camera_back ef68 +photo_camera_front ef69 +photo_filter e43b +photo_library e413 +photo_size_select_actual e432 +photo_size_select_large e433 +photo_size_select_small e434 +php eb8f +piano e521 +piano_off e520 +picture_as_pdf e415 +picture_in_picture e8aa +picture_in_picture_alt e911 +pie_chart e6c4 +pie_chart_outline f044 +pie_chart_outlined e6c5 +pin f045 +pin_drop e55e +pin_end e767 +pin_invoke e763 +pinch eb38 +pivot_table_chart e9ce +pix eaa3 +place e55f +plagiarism ea5a +play_arrow e037 +play_circle e1c4 +play_circle_fill e038 +play_circle_filled e038 +play_circle_outline e039 +play_disabled ef6a +play_for_work e906 +play_lesson f047 +playlist_add e03b +playlist_add_check e065 +playlist_add_check_circle e7e6 +playlist_add_circle e7e5 +playlist_play e05f +playlist_remove eb80 +plumbing f107 +plus_one e800 +podcasts f048 +point_of_sale f17e +policy ea17 +poll e801 +polyline ebbb +polymer e8ab +pool eb48 +portable_wifi_off e0ce +portrait e416 +post_add ea20 +power e63c +power_input e336 +power_off e646 +power_settings_new e8ac +precision_manufacturing f049 +pregnant_woman e91e +present_to_all e0df +preview f1c5 +price_change f04a +price_check f04b +print e8ad +print_disabled e9cf +priority_high e645 +privacy_tip f0dc +private_connectivity e744 +production_quantity_limits e1d1 +psychology ea4a +public e80b +public_off f1ca +publish e255 +published_with_changes f232 +punch_clock eaa8 +push_pin f10d +qr_code ef6b +qr_code_2 e00a +qr_code_scanner f206 +query_builder e8ae +query_stats e4fc +question_answer e8af +question_mark eb8b +queue e03c +queue_music e03d +queue_play_next e066 +quick_contacts_dialer e0cf +quick_contacts_mail e0d0 +quickreply ef6c +quiz f04c +quora ea98 +r_mobiledata f04d +radar f04e +radio e03e +radio_button_checked e837 +radio_button_off e836 +radio_button_on e837 +radio_button_unchecked e836 +railway_alert e9d1 +ramen_dining ea64 +ramp_left eb9c +ramp_right eb96 +rate_review e560 +raw_off f04f +raw_on f050 +read_more ef6d +real_estate_agent e73a +receipt e8b0 +receipt_long ef6e +recent_actors e03f +recommend e9d2 +record_voice_over e91f +rectangle eb54 +recycling e760 +reddit eaa0 +redeem e8b1 +redo e15a +reduce_capacity f21c +refresh e5d5 +remember_me f051 +remove e15b +remove_circle e15c +remove_circle_outline e15d +remove_done e9d3 +remove_from_queue e067 +remove_moderator e9d4 +remove_red_eye e417 +remove_shopping_cart e928 +reorder e8fe +repeat e040 +repeat_on e9d6 +repeat_one e041 +repeat_one_on e9d7 +replay e042 +replay_10 e059 +replay_30 e05a +replay_5 e05b +replay_circle_filled e9d8 +reply e15e +reply_all e15f +report e160 +report_gmailerrorred f052 +report_off e170 +report_problem e8b2 +request_page f22c +request_quote f1b6 +reset_tv e9d9 +restart_alt f053 +restaurant e56c +restaurant_menu e561 +restore e8b3 +restore_from_trash e938 +restore_page e929 +reviews f054 +rice_bowl f1f5 +ring_volume e0d1 +rocket eba5 +rocket_launch eb9b +roller_skating ebcd +roofing f201 +room e8b4 +room_preferences f1b8 +room_service eb49 +rotate_90_degrees_ccw e418 +rotate_90_degrees_cw eaab +rotate_left e419 +rotate_right e41a +roundabout_left eb99 +roundabout_right eba3 +rounded_corner e920 +route eacd +router e328 +rowing e921 +rss_feed e0e5 +rsvp f055 +rtt e9ad +rule f1c2 +rule_folder f1c9 +run_circle ef6f +running_with_errors e51d +rv_hookup e642 +safety_check ebef +safety_divider e1cc +sailing e502 +sanitizer f21d +satellite e562 +satellite_alt eb3a +save e161 +save_alt e171 +save_as eb60 +saved_search ea11 +savings e2eb +scale eb5f +scanner e329 +scatter_plot e268 +schedule e8b5 +schedule_send ea0a +schema e4fd +school e80c +science ea4b +score e269 +scoreboard ebd0 +screen_lock_landscape e1be +screen_lock_portrait e1bf +screen_lock_rotation e1c0 +screen_rotation e1c1 +screen_rotation_alt ebee +screen_search_desktop ef70 +screen_share e0e2 +screenshot f056 +scuba_diving ebce +sd e9dd +sd_card e623 +sd_card_alert f057 +sd_storage e1c2 +search e8b6 +search_off ea76 +security e32a +security_update f058 +security_update_good f059 +security_update_warning f05a +segment e94b +select_all e162 +self_improvement ea78 +sell f05b +send e163 +send_and_archive ea0c +send_time_extension eadb +send_to_mobile f05c +sensor_door f1b5 +sensor_window f1b4 +sensors e51e +sensors_off e51f +sentiment_dissatisfied e811 +sentiment_neutral e812 +sentiment_satisfied e813 +sentiment_satisfied_alt e0ed +sentiment_very_dissatisfied e814 +sentiment_very_satisfied e815 +set_meal f1ea +settings e8b8 +settings_accessibility f05d +settings_applications e8b9 +settings_backup_restore e8ba +settings_bluetooth e8bb +settings_brightness e8bd +settings_cell e8bc +settings_display e8bd +settings_ethernet e8be +settings_input_antenna e8bf +settings_input_component e8c0 +settings_input_composite e8c1 +settings_input_hdmi e8c2 +settings_input_svideo e8c3 +settings_overscan e8c4 +settings_phone e8c5 +settings_power e8c6 +settings_remote e8c7 +settings_suggest f05e +settings_system_daydream e1c3 +settings_voice e8c8 +severe_cold ebd3 +share e80d +share_arrival_time e524 +share_location f05f +shield e9e0 +shield_moon eaa9 +shop e8c9 +shop_2 e19e +shop_two e8ca +shopify ea9d +shopping_bag f1cc +shopping_basket e8cb +shopping_cart e8cc +shopping_cart_checkout eb88 +short_text e261 +shortcut f060 +show_chart e6e1 +shower f061 +shuffle e043 +shuffle_on e9e1 +shutter_speed e43d +sick f220 +sign_language ebe5 +signal_cellular_0_bar f0a8 +signal_cellular_4_bar e1c8 +signal_cellular_alt e202 +signal_cellular_alt_1_bar ebdf +signal_cellular_alt_2_bar ebe3 +signal_cellular_connected_no_internet_0_bar f0ac +signal_cellular_connected_no_internet_4_bar e1cd +signal_cellular_no_sim e1ce +signal_cellular_nodata f062 +signal_cellular_null e1cf +signal_cellular_off e1d0 +signal_wifi_0_bar f0b0 +signal_wifi_4_bar e1d8 +signal_wifi_4_bar_lock e1d9 +signal_wifi_bad f063 +signal_wifi_connected_no_internet_4 f064 +signal_wifi_off e1da +signal_wifi_statusbar_4_bar f065 +signal_wifi_statusbar_connected_no_internet_4 f066 +signal_wifi_statusbar_null f067 +signpost eb91 +sim_card e32b +sim_card_alert e624 +sim_card_download f068 +single_bed ea48 +sip f069 +skateboarding e511 +skip_next e044 +skip_previous e045 +sledding e512 +slideshow e41b +slow_motion_video e068 +smart_button f1c1 +smart_display f06a +smart_screen f06b +smart_toy f06c +smartphone e32c +smoke_free eb4a +smoking_rooms eb4b +sms e625 +sms_failed e626 +snapchat ea6e +snippet_folder f1c7 +snooze e046 +snowboarding e513 +snowing e80f +snowmobile e503 +snowshoeing e514 +soap f1b2 +social_distance e1cb +sort e164 +sort_by_alpha e053 +sos ebf7 +soup_kitchen e7d3 +source f1c4 +south f1e3 +south_america e7e4 +south_east f1e4 +south_west f1e5 +spa eb4c +space_bar e256 +space_dashboard e66b +spatial_audio ebeb +spatial_audio_off ebe8 +spatial_tracking ebea +speaker e32d +speaker_group e32e +speaker_notes e8cd +speaker_notes_off e92a +speaker_phone e0d2 +speed e9e4 +spellcheck e8ce +splitscreen f06d +spoke e9a7 +sports ea30 +sports_bar f1f3 +sports_baseball ea51 +sports_basketball ea26 +sports_cricket ea27 +sports_esports ea28 +sports_football ea29 +sports_golf ea2a +sports_gymnastics ebc4 +sports_handball ea33 +sports_hockey ea2b +sports_kabaddi ea34 +sports_martial_arts eae9 +sports_mma ea2c +sports_motorsports ea2d +sports_rugby ea2e +sports_score f06e +sports_soccer ea2f +sports_tennis ea32 +sports_volleyball ea31 +square eb36 +square_foot ea49 +ssid_chart eb66 +stacked_bar_chart e9e6 +stacked_line_chart f22b +stadium eb90 +stairs f1a9 +star e838 +star_border e83a +star_border_purple500 f099 +star_half e839 +star_outline f06f +star_purple500 f09a +star_rate f0ec +stars e8d0 +start e089 +stay_current_landscape e0d3 +stay_current_portrait e0d4 +stay_primary_landscape e0d5 +stay_primary_portrait e0d6 +sticky_note_2 f1fc +stop e047 +stop_circle ef71 +stop_screen_share e0e3 +storage e1db +store e8d1 +store_mall_directory e563 +storefront ea12 +storm f070 +straight eb95 +straighten e41c +stream e9e9 +streetview e56e +strikethrough_s e257 +stroller f1ae +style e41d +subdirectory_arrow_left e5d9 +subdirectory_arrow_right e5da +subject e8d2 +subscript f111 +subscriptions e064 +subtitles e048 +subtitles_off ef72 +subway e56f +summarize f071 +sunny e81a +sunny_snowing e819 +superscript f112 +supervised_user_circle e939 +supervisor_account e8d3 +support ef73 +support_agent f0e2 +surfing e515 +surround_sound e049 +swap_calls e0d7 +swap_horiz e8d4 +swap_horizontal_circle e933 +swap_vert e8d5 +swap_vert_circle e8d6 +swap_vertical_circle e8d6 +swipe e9ec +swipe_down eb53 +swipe_down_alt eb30 +swipe_left eb59 +swipe_left_alt eb33 +swipe_right eb52 +swipe_right_alt eb56 +swipe_up eb2e +swipe_up_alt eb35 +swipe_vertical eb51 +switch_access_shortcut e7e1 +switch_access_shortcut_add e7e2 +switch_account e9ed +switch_camera e41e +switch_left f1d1 +switch_right f1d2 +switch_video e41f +synagogue eab0 +sync e627 +sync_alt ea18 +sync_disabled e628 +sync_lock eaee +sync_problem e629 +system_security_update f072 +system_security_update_good f073 +system_security_update_warning f074 +system_update e62a +system_update_alt e8d7 +system_update_tv e8d7 +tab e8d8 +tab_unselected e8d9 +table_bar ead2 +table_chart e265 +table_restaurant eac6 +table_rows f101 +table_view f1be +tablet e32f +tablet_android e330 +tablet_mac e331 +tag e9ef +tag_faces e420 +takeout_dining ea74 +tap_and_play e62b +tapas f1e9 +task f075 +task_alt e2e6 +taxi_alert ef74 +telegram ea6b +temple_buddhist eab3 +temple_hindu eaaf +terminal eb8e +terrain e564 +text_decrease eadd +text_fields e262 +text_format e165 +text_increase eae2 +text_rotate_up e93a +text_rotate_vertical e93b +text_rotation_angledown e93c +text_rotation_angleup e93d +text_rotation_down e93e +text_rotation_none e93f +text_snippet f1c6 +textsms e0d8 +texture e421 +theater_comedy ea66 +theaters e8da +thermostat f076 +thermostat_auto f077 +thumb_down e8db +thumb_down_alt e816 +thumb_down_off_alt e9f2 +thumb_up e8dc +thumb_up_alt e817 +thumb_up_off_alt e9f3 +thumbs_up_down e8dd +thunderstorm ebdb +tiktok ea7e +time_to_leave e62c +timelapse e422 +timeline e922 +timer e425 +timer_10 e423 +timer_10_select f07a +timer_3 e424 +timer_3_select f07b +timer_off e426 +tips_and_updates e79a +tire_repair ebc8 +title e264 +toc e8de +today e8df +toggle_off e9f5 +toggle_on e9f6 +token ea25 +toll e8e0 +tonality e427 +topic f1c8 +touch_app e913 +tour ef75 +toys e332 +track_changes e8e1 +traffic e565 +train e570 +tram e571 +transfer_within_a_station e572 +transform e428 +transgender e58d +transit_enterexit e579 +translate e8e2 +travel_explore e2db +trending_down e8e3 +trending_flat e8e4 +trending_neutral e8e4 +trending_up e8e5 +trip_origin e57b +try f07c +tsunami ebd8 +tty f1aa +tune e429 +tungsten f07d +turn_left eba6 +turn_right ebab +turn_sharp_left eba7 +turn_sharp_right ebaa +turn_slight_left eba4 +turn_slight_right eb9a +turned_in e8e6 +turned_in_not e8e7 +tv e333 +tv_off e647 +two_wheeler e9f9 +u_turn_left eba1 +u_turn_right eba2 +umbrella f1ad +unarchive e169 +undo e166 +unfold_less e5d6 +unfold_more e5d7 +unpublished f236 +unsubscribe e0eb +upcoming f07e +update e923 +update_disabled e075 +upgrade f0fb +upload f09b +upload_file e9fc +usb e1e0 +usb_off e4fa +vaccines e138 +vape_free ebc6 +vaping_rooms ebcf +verified ef76 +verified_user e8e8 +vertical_align_bottom e258 +vertical_align_center e259 +vertical_align_top e25a +vertical_distribute e076 +vertical_split e949 +vibration e62d +video_call e070 +video_camera_back f07f +video_camera_front f080 +video_collection e04a +video_file eb87 +video_label e071 +video_library e04a +video_settings ea75 +video_stable f081 +videocam e04b +videocam_off e04c +videogame_asset e338 +videogame_asset_off e500 +view_agenda e8e9 +view_array e8ea +view_carousel e8eb +view_column e8ec +view_comfortable e42a +view_comfy e42a +view_comfy_alt eb73 +view_compact e42b +view_compact_alt eb74 +view_cozy eb75 +view_day e8ed +view_headline e8ee +view_in_ar e9fe +view_kanban eb7f +view_list e8ef +view_module e8f0 +view_quilt e8f1 +view_sidebar f114 +view_stream e8f2 +view_timeline eb85 +view_week e8f3 +vignette e435 +villa e586 +visibility e8f4 +visibility_off e8f5 +voice_chat e62e +voice_over_off e94a +voicemail e0d9 +volcano ebda +volume_down e04d +volume_down_alt e79c +volume_mute e04e +volume_off e04f +volume_up e050 +volunteer_activism ea70 +vpn_key e0da +vpn_key_off eb7a +vpn_lock e62f +vrpano f082 +wallet_giftcard e8f6 +wallet_membership e8f7 +wallet_travel e8f8 +wallpaper e1bc +warehouse ebb8 +warning e002 +warning_amber f083 +wash f1b1 +watch e334 +watch_later e924 +watch_off eae3 +water f084 +water_damage f203 +water_drop e798 +waterfall_chart ea00 +waves e176 +waving_hand e766 +wb_auto e42c +wb_cloudy e42d +wb_incandescent e42e +wb_iridescent e436 +wb_shade ea01 +wb_sunny e430 +wb_twighlight ea02 +wb_twilight e1c6 +wc e63d +web e051 +web_asset e069 +web_asset_off e4f7 +web_stories e595 +webhook eb92 +wechat ea81 +weekend e16b +west f1e6 +whatsapp ea9c +whatshot e80e +wheelchair_pickup f1ab +where_to_vote e177 +widgets e1bd +wifi e63e +wifi_1_bar e4ca +wifi_2_bar e4d9 +wifi_calling ef77 +wifi_calling_3 f085 +wifi_channel eb6a +wifi_find eb31 +wifi_lock e1e1 +wifi_off e648 +wifi_password eb6b +wifi_protected_setup f0fc +wifi_tethering e1e2 +wifi_tethering_error ead9 +wifi_tethering_error_rounded f086 +wifi_tethering_off f087 +window f088 +wine_bar f1e8 +woman e13e +woo_commerce ea6d +wordpress ea9f +work e8f9 +work_off e942 +work_outline e943 +workspace_premium e7af +workspaces e1a0 +workspaces_filled ea0d +workspaces_outline ea0f +wrap_text e25b +wrong_location ef78 +wysiwyg f1c3 +yard f089 +youtube_searched_for e8fa +zoom_in e8ff +zoom_in_map eb2d +zoom_out e900 +zoom_out_map e56b diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index f8a4ad279b..bb74a5f2e3 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -41,7 +41,7 @@ import { alarmFields } from '@shared/models/alarm.models'; import { materialColors } from '@app/shared/models/material.models'; import { WidgetInfo } from '@home/models/widget-component.models'; import jsonSchemaDefaults from 'json-schema-defaults'; -import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints'; +import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw'; import { Observable, of, ReplaySubject } from 'rxjs'; const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); diff --git a/ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf b/ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..be4be29c8664ae8199bd0fa6c8df9e8e140d354f GIT binary patch literal 337868 zcmb@v2Y3}#*SEiB=Iq%+Z%Kgk(3Bzyf>JElP_Ro;5d{P*h$xB`#fG4$h=7P8mTd(BP+pZEFR|Mgwhmvi0u&FtA_?X}kInKS1E#1oMk zGC~3wa_-sZbT*Zwn~0Z0Y5KXBT-;5Pr84^w`&G~F*5l0N%l14hazhJ|f_fKsJHB=0 zp$+}}5R@f<(tMhb$%euDiW>^HjcmOJh)x@^xCBEefC zHRlbwwST|rkIsEcB&n}R&W*S9yL(79$GepJCs03oaKBrwODk(TLd3^j@7^J|4!z^> zNNLUXC3?KvHV>Qp*rdwLmD$GIRZ;!%c?EI|WydBCxO&l*UbfULKdQBLI-R6tT*B=1 z=kOys&VTu#t{)fsa`;fZ9XsQwnnB#3m*}W`SpIffKN_}}$$1>&Cam=f&4d9=j5pu%e!xgs0rAJ$q zEyP8v^uilnq7vJEWl{OIC+RY+eA}1mGO>I+kjsR~#`P0N)iIJP&C1I|wnQ$JesYIg zC%4N@Yz>kw(qC?s!7@~iqmS$42Dy{cejM)^*BVMaJ)SMC@cRk;K3O`)C3nPnJ1wr` zXzTcBwsc_oKgY;EY7|$aEu)Yfs|HsJxTl=#Zdx+c<+kb=H$Xs-6wW}Rl=W_H` zEET|Kz7RQB9Uk z(gW=jt8TabXK$bkq*QCUn$2ZIyfyQmGCit2XuBR9)#I*?_Sl^E6XK(~*MOQ@tKyZ+ zeslH|x2=68<0IFrZflKB{MHfFhEB}8hunn@j9XTeYA-sPt2Z#-Yfd!vxcj+^UIxZ! zO^N%|R?XPbQS`@oLVMA&PSTz7PU6!}%PPj#aoo0+IZIA0Ki*t&{yD1s>C4qQ(n3db zeN@cL)pEV+v5MCOyyspzeh;Wo{!*Xm9FDwnoG5Oo*6U0?>bS8^q&%6UmEsz@HmFS# zI6JecIYAz0PhV25Ztv*d6KJ8HcQr=Pt}P`!Xsxw076Xa9+R!8JTl>E&-qK@gyL(-Z zCd>J8J?%l;>uj}7+wwiN?#Pe&zQP|mTQ_T$WjeNQ&6JPh(MQBJb@mlMIMcorNQ?v3cwp?NQ5gd}oETC|PQY zMnwCm7uV3^I*%*S%(%q$ND+0kmkNt7i0x6Ej<&+J)*f{&8}lCa^sLZO%HZ zd83h5PiY@LZ2m4)HNJg6mJWHTx^d)H)r-mAm#@=JuU8owY}7uGshD zTFwp~uemggd)E=P^vFxI&YeL$xU##tI+nK4+39#)SXW$M75%GSdc@7&m1z< zxHkG*YrAq+s&(B65|1I&Q9oBfD~>qt4P?I)*K9qmkxb?~>AX5HR_(i290ga}T-wAf zbTnt3D_6fe?{-49Z$0kLauR~Huf6JdnoM1_Lyzcht*icbedrq2i5|6;*6+kBIxt=bI&t^Yg?+W42PK-{6?Ujr zc-NKvzx-V>1829h*_G*bCoEPwwM6Hx+ZAzkwX{UbbgnvEUEQ>m`Y(#hbj$UnTW

?Ybv9RcK)s_@ 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 G{l5W@A9e8n literal 0 HcmV?d00001 diff --git a/ui-ngx/src/assets/fonts/material-icons.css b/ui-ngx/src/assets/fonts/material-icons.css new file mode 100644 index 0000000000..fe6f64850d --- /dev/null +++ b/ui-ngx/src/assets/fonts/material-icons.css @@ -0,0 +1,33 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/ui-ngx/src/index.html b/ui-ngx/src/index.html index 946d96f1b7..da8d9f6568 100644 --- a/ui-ngx/src/index.html +++ b/ui-ngx/src/index.html @@ -24,7 +24,7 @@ - +