Browse Source

Replacement of local cache in rule nodes with cached entity service APIs. (#9552)

* added customer cache && refactored assign/unassing to/from customer nodes

* refactoring of customer action nodes & added common tests

* added tests for upgrade

* updated controller & service tests for customer api

* replaced findCustomerByTenantIdAndTitle with cacheable method

* add new line to end of ../upgrade/3.6.2/schema_update.sql file

* added cache for find user by tenant id and email & relation action nodes refactoring

* fixed synchronization issue & refactored logic & refactored node upgrade tests

* fixed testSaveUserWithSameEmail test in the UserControllerTest. Removed empty space & added exclamation mark

* updated tests for create relation node && added two tests for delete relation node

* replaced Lists with EnumSets where it was possible & replaced MethodSource with EnumSource where it was possible & added test for upgrade for both create and delete relation nodes

* updated upgrade tests & added tests for delete relation node

* refactoring

* updated create & delete relation nodes descriptions

* updated default config for delete relation node

* minor improvements for custom action nodes after self review

* added upgrade for customers with the same titles

* replaced parameterized test annotation when deleteForSingleEntity is false

* fix typo in reSaveCustomerWithNewTitle upgrade logic

* fixes after review & additional improvements

* fixed testAssignAndUnassignEntityViewToCustomer test

* updated upgrade logic & changed logic of public customers lookup

* fixed dao method & added test

* renamed method to findPublicCustomerByTenantId

* fix typo in the find public customer query

* added @Serial annotation for serialVersionUID & rollback saveAndFlush to save in UserServiceImpl

* fixes to async find or create logic

* fixed after review

* fix typo

* set Transactional annotation to findOrCreatePublicCustomer
pull/10527/head
Shvaika Dmytro 2 years ago
committed by GitHub
parent
commit
cad3011938
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 99
      application/src/main/data/upgrade/3.6.3/schema_update.sql
  2. 6
      application/src/main/resources/thingsboard.yml
  3. 27
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  4. 13
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  5. 2
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
  6. 30
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java
  7. 44
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheKey.java
  8. 33
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCaffeineCache.java
  9. 35
      common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerRedisCache.java
  10. 14
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheEvictEvent.java
  11. 44
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheKey.java
  12. 33
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserCaffeineCache.java
  13. 35
      common/cache/src/main/java/org/thingsboard/server/cache/user/UserRedisCache.java
  14. 5
      common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  16. 6
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  17. 12
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
  18. 80
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  19. 8
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  20. 6
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  21. 8
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  22. 6
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  23. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  24. 4
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java
  25. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java
  26. 5
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  27. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
  28. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
  29. 64
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  30. 2
      dao/src/main/resources/sql/schema-entities.sql
  31. 26
      dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java
  32. 43
      dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java
  33. 29
      dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java
  34. 6
      dao/src/test/resources/application-test.properties
  35. 148
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java
  36. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNodeConfiguration.java
  37. 440
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  38. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNodeConfiguration.java
  39. 80
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java
  40. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeConfiguration.java
  41. 254
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
  42. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeConfiguration.java
  43. 110
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
  44. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java
  45. 82
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java
  46. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeConfiguration.java
  47. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  48. 335
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeTest.java
  49. 747
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java
  50. 604
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java
  51. 313
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java
  52. 37
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java

99
application/src/main/data/upgrade/3.6.3/schema_update.sql

@ -14,6 +14,105 @@
-- limitations under the License.
--
-- UPDATE PUBLIC CUSTOMERS START
ALTER TABLE customer ADD COLUMN IF NOT EXISTS is_public boolean DEFAULT false;
UPDATE customer SET is_public = true WHERE title = 'Public';
-- UPDATE PUBLIC CUSTOMERS END
-- UPDATE CUSTOMERS WITH SAME TITLE START
CREATE OR REPLACE PROCEDURE update_customers_with_the_same_title()
LANGUAGE plpgsql
AS
$$
DECLARE
customer_record RECORD;
dashboard_record RECORD;
title_exists BOOLEAN;
new_title TEXT;
updated_json JSONB;
BEGIN
RAISE NOTICE 'Starting the customer and dashboard update process.';
FOR customer_record IN
SELECT id, tenant_id, title, duplicate_number
FROM (
SELECT
id,
tenant_id,
title,
ROW_NUMBER() OVER(PARTITION BY tenant_id, title ORDER BY id) AS duplicate_number
FROM customer
) AS duplicate_customers
WHERE duplicate_number > 1
LOOP
-- Attempt with 'duplicate' suffix
new_title := customer_record.title || ' duplicate ' || (customer_record.duplicate_number - 1)::TEXT;
-- Check if new_title already exists for the same tenant_id
SELECT EXISTS (
SELECT 1
FROM customer
WHERE tenant_id = customer_record.tenant_id
AND title = new_title
) INTO title_exists;
-- If generated title exists, use customer id instead to create a unique title
IF title_exists THEN
new_title := customer_record.title || ' duplicate ' || customer_record.id::TEXT;
END IF;
-- Update the customer title
UPDATE customer
SET title = new_title
WHERE id = customer_record.id;
RAISE NOTICE 'Updated customer with id: % with new title: %', customer_record.id, new_title;
-- Find and update related dashboards for the customer
FOR dashboard_record IN
SELECT d.id, d.assigned_customers
FROM dashboard d
JOIN relation r ON d.id = r.to_id
WHERE r.from_id = customer_record.id
AND r.to_type = 'DASHBOARD'
AND r.relation_type_group = 'DASHBOARD'
AND r.relation_type = 'Contains'
LOOP
-- Update each assigned_customers entry where the customerId matches
updated_json := (SELECT jsonb_agg(
CASE
WHEN (value -> 'customerId' ->> 'id')::uuid = customer_record.id
THEN jsonb_set(value, '{title}', ('"' || new_title || '"')::jsonb)
ELSE value
END
)
FROM jsonb_array_elements(dashboard_record.assigned_customers::jsonb));
UPDATE dashboard
SET assigned_customers = updated_json
WHERE id = dashboard_record.id;
RAISE NOTICE 'Updated dashboard with id: % with new assigned_customers: %', dashboard_record.id, updated_json;
END LOOP;
END LOOP;
RAISE NOTICE 'Customers and dashboards update process completed successfully!';
END;
$$;
call update_customers_with_the_same_title();
DROP PROCEDURE IF EXISTS update_customers_with_the_same_title;
-- UPDATE CUSTOMERS WITH SAME TITLE END
-- CUSTOMER UNIQUE CONSTRAINT UPDATE START
ALTER TABLE customer DROP CONSTRAINT IF EXISTS customer_title_unq_key;
ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE (tenant_id, title);
-- CUSTOMER UNIQUE CONSTRAINT UPDATE END
-- create new attribute_kv table schema
DO
$$

6
application/src/main/resources/thingsboard.yml

@ -505,6 +505,12 @@ cache:
assets:
timeToLiveInMinutes: "${CACHE_SPECS_ASSETS_TTL:1440}" # Asset cache TTL
maxSize: "${CACHE_SPECS_ASSETS_MAX_SIZE:10000}" # 0 means the cache is disabled
customers:
timeToLiveInMinutes: "${CACHE_SPECS_CUSTOMERS_TTL:1440}" # Customer cache TTL
maxSize: "${CACHE_SPECS_CUSTOMERS_MAX_SIZE:10000}" # 0 means the cache is disabled
users:
timeToLiveInMinutes: "${CACHE_SPECS_USERS_TTL:1440}" # User cache TTL
maxSize: "${CACHE_SPECS_USERS_MAX_SIZE:10000}" # 0 means the cache is disabled
entityViews:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_VIEWS_TTL:1440}" # Entity view cache TTL
maxSize: "${CACHE_SPECS_ENTITY_VIEWS_MAX_SIZE:10000}" # 0 means the cache is disabled

27
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -353,7 +353,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
}
@Test
public void testFindCustomersByTitle() throws Exception {
public void testFindCustomersWithTitleAsTextSearch() throws Exception {
TenantId tenantId = savedTenant.getId();
String title1 = "Customer title 1";
@ -425,6 +425,31 @@ public class CustomerControllerTest extends AbstractControllerTest {
Assert.assertEquals(0, pageData.getData().size());
}
@Test
public void testFindCustomerByTitle() throws Exception {
Customer customer = new Customer();
customer.setTitle("My customer");
Mockito.reset(tbClusterService, auditLogService);
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
testNotifyEntityAllOneTime(savedCustomer, savedCustomer.getId(), savedCustomer.getId(), savedCustomer.getTenantId(),
new CustomerId(CustomerId.NULL_UUID), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED);
Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
Customer foundCustomer = doGet("/api/tenant/customers?customerTitle=" + savedCustomer.getTitle(), Customer.class);
Assert.assertEquals(foundCustomer, savedCustomer);
doDelete("/api/customer/" + savedCustomer.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testDeleteCustomerWithDeleteRelationsOk() throws Exception {
CustomerId customerId = createCustomer("Customer for Test WithRelationsOk").getId();

13
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -275,10 +275,9 @@ public class EntityViewControllerTest extends AbstractControllerTest {
}
@Test
public void testAssignAndUnAssignedEntityViewToCustomer() throws Exception {
public void testAssignAndUnassignEntityViewToCustomer() throws Exception {
EntityView view = getNewSavedEntityView("Test entity view");
Customer savedCustomer = doPost("/api/customer", getNewCustomer("My customer"), Customer.class);
view.setCustomerId(savedCustomer.getId());
Mockito.reset(tbClusterService, auditLogService);
@ -304,17 +303,17 @@ public class EntityViewControllerTest extends AbstractControllerTest {
ActionType.ASSIGNED_TO_CUSTOMER, ActionType.UPDATED,
foundView.getId().getId().toString(), foundView.getCustomerId().getId().toString(), savedCustomer.getTitle());
EntityView unAssignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unAssignedView.getCustomerId().getId());
EntityView unassignedView = doDelete("/api/customer/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, unassignedView.getCustomerId().getId());
foundView = doGet("/api/entityView/" + savedView.getId().getId().toString(), EntityView.class);
assertEquals(ModelConstants.NULL_UUID, foundView.getCustomerId().getId());
testBroadcastEntityStateChangeEventTime(foundView.getId(), foundView.getTenantId(), 1);
testNotifyAssignUnassignEntityAllOneTime(unAssignedView, savedView.getId(), savedView.getId(),
tenantId, savedView.getCustomerId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
testNotifyAssignUnassignEntityAllOneTime(unassignedView, unassignedView.getId(), unassignedView.getId(),
tenantId, savedCustomer.getId(), tenantAdminUserId, TENANT_ADMIN_EMAIL,
ActionType.UNASSIGNED_FROM_CUSTOMER, ActionType.UPDATED,
assignedView.getId().getId().toString(), savedView.getCustomerId().getId().toString(), savedCustomer.getTitle());
assignedView.getId().getId().toString(), savedCustomer.getId().getId().toString(), savedCustomer.getTitle());
}
@Test

2
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -279,7 +279,7 @@ public class UserControllerTest extends AbstractControllerTest {
user.setTenantId(tenantId);
user.setEmail(TENANT_ADMIN_EMAIL);
String msgError = "User with email '" + TENANT_ADMIN_EMAIL + "' already present in database";
String msgError = "User with email '" + TENANT_ADMIN_EMAIL + "' already present in database!";
doPost("/api/user", user)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));

30
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheEvictEvent.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.customer;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@RequiredArgsConstructor
public class CustomerCacheEvictEvent {
private final TenantId tenantId;
private final String newTitle;
private final String oldTitle;
}

44
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCacheKey.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.customer;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class CustomerCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 5706958428811356925L;
private final TenantId tenantId;
private final String title;
@Override
public String toString() {
return tenantId + "_" + title;
}
}

33
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.customer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Customer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("CustomerCache")
public class CustomerCaffeineCache extends CaffeineTbTransactionalCache<CustomerCacheKey, Customer> {
public CustomerCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.CUSTOMER_CACHE);
}
}

35
common/cache/src/main/java/org/thingsboard/server/cache/customer/CustomerRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.customer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.Customer;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("CustomerCache")
public class CustomerRedisCache extends RedisTbTransactionalCache<CustomerCacheKey, Customer> {
public CustomerRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.CUSTOMER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(Customer.class));
}
}

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityContainer.java → common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheEvictEvent.java

@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
package org.thingsboard.server.cache.user;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class EntityContainer {
@RequiredArgsConstructor
public class UserCacheEvictEvent {
private EntityId entityId;
private EntityType entityType;
private final TenantId tenantId;
private final String newEmail;
private final String oldEmail;
}

44
common/cache/src/main/java/org/thingsboard/server/cache/user/UserCacheKey.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.user;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class UserCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 7357353074893750678L;
private final TenantId tenantId;
private final String email;
@Override
public String toString() {
return tenantId + "_" + email;
}
}

33
common/cache/src/main/java/org/thingsboard/server/cache/user/UserCaffeineCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.User;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("UserCache")
public class UserCaffeineCache extends CaffeineTbTransactionalCache<UserCacheKey, User> {
public UserCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USER_CACHE);
}
}

35
common/cache/src/main/java/org/thingsboard/server/cache/user/UserRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.user;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbJsonRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.User;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("UserCache")
public class UserRedisCache extends RedisTbTransactionalCache<UserCacheKey, User> {
public UserRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(User.class));
}
}

5
common/cache/src/test/java/org/thingsboard/server/cache/CacheSpecsMapTest.java

@ -26,6 +26,7 @@ import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.thingsboard.server.common.data.CacheConstants;
import static org.assertj.core.api.Assertions.assertThat;
@ -53,8 +54,8 @@ public class CacheSpecsMapTest {
@Test
public void givenCacheConfig_whenCacheManagerReady_thenVerifyExistedCachesWithNoTransactionAwareCacheDecorator() {
// We no longer use built-in transaction support for the caches, because we have our own cache cleanup and transaction logic that implements CAS.
assertThat(cacheManager.getCache("relations")).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache("devices")).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache(CacheConstants.RELATIONS_CACHE)).isInstanceOf(CaffeineCache.class);
assertThat(cacheManager.getCache(CacheConstants.DEVICE_CACHE)).isInstanceOf(CaffeineCache.class);
}
@Test

2
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -21,6 +21,8 @@ public class CacheConstants {
public static final String DEVICE_CACHE = "devices";
public static final String SESSIONS_CACHE = "sessions";
public static final String ASSET_CACHE = "assets";
public static final String CUSTOMER_CACHE = "customers";
public static final String USER_CACHE = "users";
public static final String ENTITY_VIEW_CACHE = "entityViews";
public static final String EDGE_CACHE = "edges";
public static final String CLAIM_DEVICES_CACHE = "claimDevices";

6
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -184,6 +184,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId) {
Asset asset = findAssetById(tenantId, assetId);
if (customerId.equals(asset.getCustomerId())) {
return asset;
}
asset.setCustomerId(customerId);
return saveAsset(asset);
}
@ -191,6 +194,9 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId) {
Asset asset = findAssetById(tenantId, assetId);
if (asset.getCustomerId() == null) {
return asset;
}
asset.setCustomerId(null);
return saveAsset(asset);
}

12
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java

@ -50,12 +50,20 @@ public interface CustomerDao extends Dao<Customer>, TenantEntityDao, ExportableE
PageData<Customer> findCustomersByTenantId(UUID tenantId, PageLink pageLink);
/**
* Find customers by tenantId and customer title.
* Find customer by tenantId and customer title.
*
* @param tenantId the tenantId
* @param title the customer title
* @return the optional customer object
*/
Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title);
Optional<Customer> findCustomerByTenantIdAndTitle(UUID tenantId, String title);
/**
* Find public customer by tenantId.
*
* @param tenantId the tenantId
* @return the optional public customer object
*/
Optional<Customer> findPublicCustomerByTenantId(UUID tenantId);
}

80
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -15,15 +15,21 @@
*/
package org.thingsboard.server.dao.customer;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.customer.CustomerCacheEvictEvent;
import org.thingsboard.server.cache.customer.CustomerCacheKey;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
@ -33,7 +39,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
@ -44,17 +50,24 @@ import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("CustomerDaoService")
@Slf4j
public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCacheKey, Customer, CustomerCacheEvictEvent> implements CustomerService {
public static final String PUBLIC_CUSTOMER_TITLE = "Public";
public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String PUBLIC_CUSTOMER_ADDITIONAL_INFO_STR = "{ \"isPublic\": true }";
public static final JsonNode PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON = JacksonUtil.toJsonNode(PUBLIC_CUSTOMER_ADDITIONAL_INFO_STR);
private final ConcurrentMap<TenantId, Object> publicCustomerCreationLocks = new ConcurrentReferenceHashMap<>();
@Autowired
private CustomerDao customerDao;
@ -81,6 +94,17 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
@Autowired
private EntityCountService countService;
@TransactionalEventListener(classes = CustomerCacheEvictEvent.class)
@Override
public void handleEvictEvent(CustomerCacheEvictEvent event) {
List<CustomerCacheKey> keys = new ArrayList<>(2);
keys.add(new CustomerCacheKey(event.getTenantId(), event.getNewTitle()));
if (StringUtils.isNotEmpty(event.getOldTitle()) && !event.getOldTitle().equals(event.getNewTitle())) {
keys.add(new CustomerCacheKey(event.getTenantId(), event.getOldTitle()));
}
cache.evict(keys);
}
@Override
public Customer findCustomerById(TenantId tenantId, CustomerId customerId) {
log.trace("Executing findCustomerById [{}]", customerId);
@ -92,7 +116,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
public Optional<Customer> findCustomerByTenantIdAndTitle(TenantId tenantId, String title) {
log.trace("Executing findCustomerByTenantIdAndTitle [{}] [{}]", tenantId, title);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), title);
return Optional.ofNullable(cache.getAndPutInTransaction(new CustomerCacheKey(tenantId, title),
() -> customerDao.findCustomerByTenantIdAndTitle(tenantId.getId(), title)
.orElse(null), true));
}
@Override
@ -103,23 +129,38 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
}
@Override
@Transactional
public Customer saveCustomer(Customer customer) {
return saveCustomer(customer, true);
}
private Customer saveCustomer(Customer customer, boolean doValidate) {
log.trace("Executing saveCustomer [{}]", customer);
customerValidator.validate(customer, Customer::getTenantId);
String oldCustomerTitle = null;
if (doValidate) {
Customer oldCustomer = customerValidator.validate(customer, Customer::getTenantId);
if (oldCustomer != null) {
oldCustomerTitle = oldCustomer.getTitle();
}
}
var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle);
try {
Customer savedCustomer = customerDao.save(customer.getTenantId(), customer);
Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer);
dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId());
if (customer.getId() == null) {
countService.publishCountEntityEvictEvent(savedCustomer.getTenantId(), EntityType.CUSTOMER);
}
publishEvictEvent(evictEvent);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCustomer.getTenantId())
.entityId(savedCustomer.getId()).created(customer.getId() == null).build());
return savedCustomer;
} catch (Exception e) {
checkConstraintViolation(e, "customer_external_id_unq_key", "Customer with such external id already exists!");
handleEvictEvent(evictEvent);
checkConstraintViolation(e,
"customer_title_unq_key", "Customer with such title already exists!",
"customer_external_id_unq_key", "Customer with such external id already exists!");
throw e;
}
}
@Override
@ -142,27 +183,32 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
customerDao.removeById(tenantId, customerId.getId());
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(customerId).build());
publishEvictEvent(new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), null));
}
@Override
@Transactional
public Customer findOrCreatePublicCustomer(TenantId tenantId) {
log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
Validator.validateId(tenantId, id -> INCORRECT_CUSTOMER_ID + id);
Optional<Customer> publicCustomerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE);
Validator.validateId(tenantId, id -> INCORRECT_TENANT_ID + tenantId);
Optional<Customer> publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get();
} else {
Customer publicCustomer = new Customer();
}
synchronized (publicCustomerCreationLocks.computeIfAbsent(tenantId, k -> new Object())) {
publicCustomerOpt = customerDao.findPublicCustomerByTenantId(tenantId.getId());
if (publicCustomerOpt.isPresent()) {
return publicCustomerOpt.get();
}
var publicCustomer = new Customer();
publicCustomer.setTenantId(tenantId);
publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
try {
publicCustomer.setAdditionalInfo(JacksonUtil.toJsonNode("{ \"isPublic\": true }"));
publicCustomer.setAdditionalInfo(PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON);
} catch (IllegalArgumentException e) {
throw new IncorrectParameterException("Unable to create public customer.", e);
}
Customer savedCustomer = customerDao.save(tenantId, publicCustomer);
countService.publishCountEntityEvictEvent(tenantId, EntityType.CUSTOMER);
return savedCustomer;
return saveCustomer(publicCustomer, false);
}
}
@ -181,8 +227,8 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom
customersByTenantRemover.removeEntities(tenantId, tenantId);
}
private PaginatedRemover<TenantId, Customer> customersByTenantRemover =
new PaginatedRemover<TenantId, Customer>() {
private final PaginatedRemover<TenantId, Customer> customersByTenantRemover =
new PaginatedRemover<>() {
@Override
protected PageData<Customer> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {

8
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -171,8 +171,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
}
}
@Override
public Dashboard assignDashboardToCustomer(TenantId tenantId, DashboardId dashboardId, CustomerId customerId) {
Dashboard dashboard = findDashboardById(tenantId, dashboardId);
@ -216,12 +214,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
}
}
private Dashboard updateAssignedCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer) {
private void updateAssignedCustomer(TenantId tenantId, DashboardId dashboardId, Customer customer) {
Dashboard dashboard = findDashboardById(tenantId, dashboardId);
if (dashboard.updateAssignedCustomer(customer)) {
return saveDashboard(dashboard);
} else {
return dashboard;
saveDashboard(dashboard);
}
}

6
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -306,6 +306,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Override
public Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId) {
Device device = findDeviceById(tenantId, deviceId);
if (customerId.equals(device.getCustomerId())) {
return device;
}
device.setCustomerId(customerId);
return saveDevice(device);
}
@ -314,6 +317,9 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Override
public Device unassignDeviceFromCustomer(TenantId tenantId, DeviceId deviceId) {
Device device = findDeviceById(tenantId, deviceId);
if (device.getCustomerId() == null) {
return device;
}
device.setCustomerId(null);
return saveDevice(device);
}

8
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -199,6 +199,9 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
public Edge assignEdgeToCustomer(TenantId tenantId, EdgeId edgeId, CustomerId customerId) {
log.trace("[{}] Executing assignEdgeToCustomer [{}][{}]", tenantId, edgeId, customerId);
Edge edge = findEdgeById(tenantId, edgeId);
if (customerId.equals(edge.getCustomerId())) {
return edge;
}
edge.setCustomerId(customerId);
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(edgeId)
.body(JacksonUtil.toString(customerId)).actionType(ActionType.ASSIGNED_TO_CUSTOMER).build());
@ -209,7 +212,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
public Edge unassignEdgeFromCustomer(TenantId tenantId, EdgeId edgeId) {
log.trace("[{}] Executing unassignEdgeFromCustomer [{}]", tenantId, edgeId);
Edge edge = findEdgeById(tenantId, edgeId);
var customerId = edge.getCustomerId();
CustomerId customerId = edge.getCustomerId();
if (customerId == null) {
return edge;
}
edge.setCustomerId(null);
Edge result = saveEdge(edge);
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(edgeId)

6
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -131,6 +131,9 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
@Override
public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
if (customerId.equals(entityView.getCustomerId())) {
return entityView;
}
entityView.setCustomerId(customerId);
return saveEntityView(entityView);
}
@ -138,6 +141,9 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
@Override
public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId) {
EntityView entityView = findEntityViewById(tenantId, entityViewId);
if (entityView.getCustomerId() == null) {
return entityView;
}
entityView.setCustomerId(null);
return saveEntityView(entityView);
}

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -135,6 +135,7 @@ public class ModelConstants {
public static final String CUSTOMER_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String CUSTOMER_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String CUSTOMER_IS_PUBLIC_PROPERTY = "is_public";
/**
* Device constants.

4
dao/src/main/java/org/thingsboard/server/dao/model/sql/CustomerEntity.java

@ -67,6 +67,9 @@ public final class CustomerEntity extends BaseSqlEntity<Customer> {
@Column(name = ModelConstants.EMAIL_PROPERTY)
private String email;
@Column(name = ModelConstants.CUSTOMER_IS_PUBLIC_PROPERTY)
private boolean isPublic;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.CUSTOMER_ADDITIONAL_INFO_PROPERTY)
private JsonNode additionalInfo;
@ -94,6 +97,7 @@ public final class CustomerEntity extends BaseSqlEntity<Customer> {
this.phone = customer.getPhone();
this.email = customer.getEmail();
this.additionalInfo = customer.getAdditionalInfo();
this.isPublic = customer.isPublic();
if (customer.getExternalId() != null) {
this.externalId = customer.getExternalId().getId();
}

21
dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java

@ -27,8 +27,6 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Optional;
@Component
public class CustomerDataValidator extends DataValidator<Customer> {
@ -41,24 +39,15 @@ public class CustomerDataValidator extends DataValidator<Customer> {
@Override
protected void validateCreate(TenantId tenantId, Customer customer) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.CUSTOMER);
customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
c -> {
throw new DataValidationException("Customer with such title already exists!");
}
);
}
@Override
protected Customer validateUpdate(TenantId tenantId, Customer customer) {
Optional<Customer> customerOpt = customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle());
customerOpt.ifPresent(
c -> {
if (!c.getId().equals(customer.getId())) {
throw new DataValidationException("Customer with such title already exists!");
}
}
);
return customerOpt.orElse(null);
Customer old = customerDao.findById(customer.getTenantId(), customer.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing customer!");
}
return old;
}
@Override

5
dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java

@ -122,11 +122,6 @@ public class UserDataValidator extends DataValidator<User> {
break;
}
User existentUserWithEmail = userService.findUserByEmail(tenantId, user.getEmail());
if (existentUserWithEmail != null && !isSameData(existentUserWithEmail, user)) {
throw new DataValidationException("User with email '" + user.getEmail() + "' "
+ " already present in database!");
}
if (!tenantId.getId().equals(ModelConstants.NULL_UUID)) {
if (!tenantService.tenantExists(user.getTenantId())) {
throw new DataValidationException("User is referencing to non-existent tenant!");

5
dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java

@ -38,6 +38,11 @@ public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID>,
CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title);
@Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " +
"AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true)
CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId);
Long countByTenantId(UUID tenantId);
@Query("SELECT externalId FROM CustomerEntity WHERE id = :id")

13
dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java

@ -30,7 +30,6 @@ import org.thingsboard.server.dao.model.sql.CustomerEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -63,9 +62,13 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
}
@Override
public Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
Customer customer = DaoUtil.getData(customerRepository.findByTenantIdAndTitle(tenantId, title));
return Optional.ofNullable(customer);
public Optional<Customer> findCustomerByTenantIdAndTitle(UUID tenantId, String title) {
return Optional.ofNullable(DaoUtil.getData(customerRepository.findByTenantIdAndTitle(tenantId, title)));
}
@Override
public Optional<Customer> findPublicCustomerByTenantId(UUID tenantId) {
return Optional.ofNullable(DaoUtil.getData(customerRepository.findPublicCustomerByTenantId(tenantId)));
}
@Override
@ -80,7 +83,7 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
@Override
public Customer findByTenantIdAndName(UUID tenantId, String name) {
return findCustomersByTenantIdAndTitle(tenantId, name).orElse(null);
return findCustomerByTenantIdAndTitle(tenantId, name).orElse(null);
}
@Override

64
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -27,8 +27,12 @@ import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.user.UserCacheEvictEvent;
import org.thingsboard.server.cache.user.UserCacheKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -47,7 +51,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
@ -56,6 +60,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -71,7 +76,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service("UserDaoService")
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends AbstractEntityService implements UserService {
public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, User, UserCacheEvictEvent> implements UserService {
public static final String USER_PASSWORD_HISTORY = "userPasswordHistory";
@ -97,6 +102,17 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final ApplicationEventPublisher eventPublisher;
private final EntityCountService countService;
@TransactionalEventListener(classes = UserCacheEvictEvent.class)
@Override
public void handleEvictEvent(UserCacheEvictEvent event) {
List<UserCacheKey> keys = new ArrayList<>(2);
keys.add(new UserCacheKey(event.getTenantId(), event.getNewEmail()));
if (StringUtils.isNotEmpty(event.getOldEmail()) && !event.getOldEmail().equals(event.getNewEmail())) {
keys.add(new UserCacheKey(event.getTenantId(), event.getOldEmail()));
}
cache.evict(keys);
}
@Override
public User findUserByEmail(TenantId tenantId, String email) {
log.trace("Executing findUserByEmail [{}]", email);
@ -113,7 +129,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
log.trace("Executing findUserByTenantIdAndEmail [{}][{}]", tenantId, email);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateString(email, e -> "Incorrect email " + e);
return userDao.findByTenantIdAndEmail(tenantId, email);
return cache.getAndPutInTransaction(new UserCacheKey(tenantId, email),
() -> userDao.findByTenantIdAndEmail(tenantId, email), true);
}
@Override
@ -131,28 +148,38 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
@Override
@Transactional
public User saveUser(TenantId tenantId, User user) {
log.trace("Executing saveUser [{}]", user);
User oldUser = userValidator.validate(user, User::getTenantId);
if (!userLoginCaseSensitive) {
user.setEmail(user.getEmail().toLowerCase());
}
User savedUser = userDao.save(user.getTenantId(), user);
if (user.getId() == null) {
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentialsDao.save(user.getTenantId(), userCredentials);
var evictEvent = new UserCacheEvictEvent(user.getTenantId(), user.getEmail(), oldUser != null ? oldUser.getEmail() : null);
User savedUser;
try {
savedUser = userDao.saveAndFlush(user.getTenantId(), user);
publishEvictEvent(evictEvent);
if (user.getId() == null) {
countService.publishCountEntityEvictEvent(savedUser.getTenantId(), EntityType.USER);
UserCredentials userCredentials = new UserCredentials();
userCredentials.setEnabled(false);
userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH));
userCredentials.setUserId(new UserId(savedUser.getUuidId()));
userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode());
userCredentialsDao.save(user.getTenantId(), userCredentials);
}
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId)
.entity(savedUser)
.oldEntity(oldUser)
.entityId(savedUser.getId())
.created(user.getId() == null).build());
} catch (Exception t) {
handleEvictEvent(evictEvent);
checkConstraintViolation(t, "tb_user_email_key", "User with email '" + user.getEmail() + "' already present in database!");
throw t;
}
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(tenantId == null ? TenantId.SYS_TENANT_ID : tenantId)
.entity(savedUser)
.oldEntity(oldUser)
.entityId(savedUser.getId())
.created(user.getId() == null).build());
return savedUser;
}
@ -263,6 +290,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
userCredentialsDao.removeByUserId(tenantId, userId);
userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId);
publishEvictEvent(new UserCacheEvictEvent(user.getTenantId(), user.getEmail(), null));
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));

2
dao/src/main/resources/sql/schema-entities.sql

@ -144,6 +144,8 @@ CREATE TABLE IF NOT EXISTS customer (
title varchar(255),
zip varchar(255),
external_id uuid,
is_public boolean,
CONSTRAINT customer_title_unq_key UNIQUE (tenant_id, title),
CONSTRAINT customer_external_id_unq_key UNIQUE (tenant_id, external_id)
);

26
dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java

@ -37,6 +37,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@ -54,7 +55,7 @@ public class CustomerServiceTest extends AbstractServiceTest {
@Before
public void before() {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
}
}
@After
public void after() {
@ -181,7 +182,7 @@ public class CustomerServiceTest extends AbstractServiceTest {
}
@Test
public void testFindCustomersByTenantIdAndTitle() throws Exception {
public void testFindCustomersByTenantIdAndTitleAsTextSearch() throws Exception {
String title1 = "Customer title 1";
List<ListenableFuture<Customer>> futures = new ArrayList<>(143);
for (int i = 0; i < 143; i++) {
@ -261,4 +262,25 @@ public class CustomerServiceTest extends AbstractServiceTest {
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
@Test
public void testFindCustomerByTitle() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle("My customer");
Customer savedCustomer = customerService.saveCustomer(customer);
Assert.assertNotNull(savedCustomer);
Assert.assertNotNull(savedCustomer.getId());
Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
Optional<Customer> foundCustomerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, savedCustomer.getTitle());
Assert.assertTrue(foundCustomerOpt.isPresent());
Assert.assertEquals(foundCustomerOpt.get().getTitle(), savedCustomer.getTitle());
customerService.deleteCustomer(tenantId, savedCustomer.getId());
}
}

43
dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java

@ -15,10 +15,10 @@
*/
package org.thingsboard.server.dao.service;
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
@ -72,7 +72,7 @@ public class UserServiceTest extends AbstractServiceTest {
customerUser.setEmail("customer@thingsboard.org");
customerUser = userService.saveUser(tenantId, customerUser);
userSettings = createUserSettings(customerUser.getId());
UserSettings userSettings = createUserSettings(customerUser.getId());
}
@Test
@ -90,6 +90,21 @@ public class UserServiceTest extends AbstractServiceTest {
Assert.assertNull(user);
}
@Test
public void testFindUserByTenantIdAndEmail() {
User user = userService.findUserByTenantIdAndEmail(SYSTEM_TENANT_ID, "sysadmin@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.SYS_ADMIN, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "tenant@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.TENANT_ADMIN, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "customer@thingsboard.org");
Assert.assertNotNull(user);
Assert.assertEquals(Authority.CUSTOMER_USER, user.getAuthority());
user = userService.findUserByTenantIdAndEmail(tenantId, "fake@thingsboard.org");
Assert.assertNull(user);
}
@Test
public void testFindUserById() {
User user = userService.findUserByEmail(SYSTEM_TENANT_ID, "sysadmin@thingsboard.org");
@ -142,36 +157,36 @@ public class UserServiceTest extends AbstractServiceTest {
public void testSaveUserWithSameEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail("sysadmin@thingsboard.org");
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("User with email 'sysadmin@thingsboard.org' already present in database!");
}
@Test
public void testSaveUserWithInvalidEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail("tenant_thingsboard.org");
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("Invalid email address format 'tenant_thingsboard.org'!");
}
@Test
public void testSaveUserWithEmptyEmail() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setEmail(null);
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("User email should be specified!");
}
@Test
public void testSaveUserWithoutTenant() {
User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org");
tenantAdminUser.setTenantId(null);
Assertions.assertThrows(DataValidationException.class, () -> {
userService.saveUser(tenantId, tenantAdminUser);
});
Assertions.assertThatThrownBy(() -> userService.saveUser(tenantId, tenantAdminUser))
.isInstanceOf(DataValidationException.class)
.hasMessage("Tenant administrator should be assigned to tenant!");
}
@Test

29
dao/src/test/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDaoTest.java

@ -19,12 +19,14 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.customer.CustomerServiceImpl;
import java.util.Optional;
import java.util.UUID;
@ -67,11 +69,27 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest {
createCustomer(tenantId, i);
}
Optional<Customer> customerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId, "CUSTOMER_5");
Optional<Customer> customerOpt = customerDao.findCustomerByTenantIdAndTitle(tenantId, "CUSTOMER_5");
assertTrue(customerOpt.isPresent());
assertEquals("CUSTOMER_5", customerOpt.get().getTitle());
}
@Test
public void testFindPublicCustomerByTenantId() {
UUID tenantId = Uuids.timeBased();
Optional<Customer> customerOpt = customerDao.findPublicCustomerByTenantId(tenantId);
assertTrue(customerOpt.isEmpty());
String publicCustomerTitle = StringUtils.randomAlphanumeric(10);
createPublicCustomer(tenantId, publicCustomerTitle);
customerOpt = customerDao.findPublicCustomerByTenantId(tenantId);
assertTrue(customerOpt.isPresent());
Customer customer = customerOpt.get();
assertTrue(customer.isPublic());
assertEquals(publicCustomerTitle, customer.getTitle());
}
private void createCustomer(UUID tenantId, int index) {
Customer customer = new Customer();
customer.setId(new CustomerId(Uuids.timeBased()));
@ -79,4 +97,13 @@ public class JpaCustomerDaoTest extends AbstractJpaDaoTest {
customer.setTitle("CUSTOMER_" + index);
customerDao.save(TenantId.fromUUID(tenantId), customer);
}
private void createPublicCustomer(UUID tenantId, String publicCustomerTitle) {
Customer customer = new Customer();
customer.setId(new CustomerId(Uuids.timeBased()));
customer.setTenantId(TenantId.fromUUID(tenantId));
customer.setTitle(publicCustomerTitle);
customer.setAdditionalInfo(CustomerServiceImpl.PUBLIC_CUSTOMER_ADDITIONAL_INFO_JSON);
customerDao.save(TenantId.fromUUID(tenantId), customer);
}
}

6
dao/src/test/resources/application-test.properties

@ -26,6 +26,12 @@ cache.specs.sessions.maxSize=100000
cache.specs.assets.timeToLiveInMinutes=1440
cache.specs.assets.maxSize=100000
cache.specs.customers.timeToLiveInMinutes=1440
cache.specs.customers.maxSize=10000
cache.specs.users.timeToLiveInMinutes=1440
cache.specs.users.maxSize=10000
cache.specs.entityViews.timeToLiveInMinutes=1440
cache.specs.entityViews.maxSize=100000

148
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java

@ -15,45 +15,52 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.Data;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.customer.CustomerService;
import java.util.EnumSet;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractCustomerActionNode<C extends TbAbstractCustomerActionNodeConfiguration> implements TbNode {
protected C config;
private ConcurrentMap<CustomerCreationLockKey, Object> customerCreationLocks;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.ASSET, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE);
private LoadingCache<CustomerKey, Optional<CustomerId>> customerIdCache;
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadCustomerNodeActionConfig(configuration);
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
if (this.config.getCustomerCacheExpiration() > 0) {
cacheBuilder.expireAfterWrite(this.config.getCustomerCacheExpiration(), TimeUnit.SECONDS);
if (createCustomerIfNotExists()) {
customerCreationLocks = new ConcurrentReferenceHashMap<>();
}
customerIdCache = cacheBuilder
.build(new CustomerCacheLoader(ctx, createCustomerIfNotExists()));
}
protected abstract boolean createCustomerIfNotExists();
@ -62,77 +69,76 @@ public abstract class TbAbstractCustomerActionNode<C extends TbAbstractCustomerA
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var entityType = msg.getOriginator().getEntityType();
if (!supportedEntityTypes.contains(entityType)) {
throw new RuntimeException(unsupportedOriginatorTypeErrorMessage(entityType));
}
withCallback(processCustomerAction(ctx, msg),
m -> ctx.tellSuccess(msg),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
ListenableFuture<CustomerId> customerIdFeature = getCustomer(ctx, msg);
return Futures.transform(customerIdFeature, customerId -> {
doProcessCustomerAction(ctx, msg, customerId);
return null;
}, ctx.getDbCallbackExecutor()
);
}
protected abstract void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId);
protected ListenableFuture<CustomerId> getCustomer(TbContext ctx, TbMsg msg) {
String customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg);
CustomerKey key = new CustomerKey(customerTitle);
return ctx.getDbCallbackExecutor().executeAsync(() -> {
Optional<CustomerId> customerId = customerIdCache.get(key);
if (!customerId.isPresent()) {
throw new RuntimeException("No customer found with name '" + key.getCustomerTitle() + "'.");
protected abstract ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg);
protected ListenableFuture<CustomerId> getCustomerIdFuture(TbContext ctx, TbMsg msg) {
var tenantId = ctx.getTenantId();
var customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg);
ListenableFuture<Optional<Customer>> customerByTitleFuture = findCustomerByTitleAsync(ctx, customerTitle);
if (createCustomerIfNotExists()) {
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
var customerCreationLockKey = new CustomerCreationLockKey(tenantId, customerTitle);
synchronized (customerCreationLocks.computeIfAbsent(customerCreationLockKey, k -> new Object())) {
customerOpt = ctx.getCustomerService().findCustomerByTenantIdAndTitle(tenantId, customerTitle);
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
var newCustomer = new Customer();
newCustomer.setTitle(customerTitle);
newCustomer.setTenantId(tenantId);
var savedCustomer = ctx.getCustomerService().saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return savedCustomer.getId();
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isEmpty()) {
throw new NoSuchElementException("Customer with title '" + customerTitle + "' doesn't exist!");
}
return customerId.get();
});
return customerOpt.get().getId();
}, MoreExecutors.directExecutor());
}
@Override
public void destroy() {
if (customerIdCache != null) {
customerIdCache.invalidateAll();
}
ListenableFuture<Optional<Customer>> findCustomerByTitleAsync(TbContext ctx, String customerTitle) {
return ctx.getDbCallbackExecutor().executeAsync(() ->
ctx.getCustomerService().findCustomerByTenantIdAndTitle(ctx.getTenantId(), customerTitle));
}
@Data
@AllArgsConstructor
private static class CustomerKey {
private String customerTitle;
private static String unsupportedOriginatorTypeErrorMessage(EntityType originatorType) {
return "Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.";
}
private static class CustomerCacheLoader extends CacheLoader<CustomerKey, Optional<CustomerId>> {
private final TbContext ctx;
private final boolean createIfNotExists;
private CustomerCacheLoader(TbContext ctx, boolean createIfNotExists) {
this.ctx = ctx;
this.createIfNotExists = createIfNotExists;
}
@Override
public Optional<CustomerId> load(CustomerKey key) {
CustomerService service = ctx.getCustomerService();
Optional<Customer> customerOptional =
service.findCustomerByTenantIdAndTitle(ctx.getTenantId(), key.getCustomerTitle());
if (customerOptional.isPresent()) {
return Optional.of(customerOptional.get().getId());
} else if (createIfNotExists) {
Customer newCustomer = new Customer();
newCustomer.setTitle(key.getCustomerTitle());
newCustomer.setTenantId(ctx.getTenantId());
Customer savedCustomer = service.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return Optional.of(savedCustomer.getId());
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) {
boolean hasChanges = false;
switch (fromVersion) {
case 0 -> {
if (oldConfiguration.has("customerCacheExpiration")) {
((ObjectNode) oldConfiguration).remove("customerCacheExpiration");
hasChanges = true;
}
}
return Optional.empty();
}
return new TbPair<>(hasChanges, oldConfiguration);
}
private record CustomerCreationLockKey(TenantId tenantId, String customerTitle) {
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNodeConfiguration.java

@ -21,6 +21,5 @@ import lombok.Data;
public abstract class TbAbstractCustomerActionNodeConfiguration {
private String customerNamePattern;
private long customerCacheExpiration;
}

440
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java

@ -15,269 +15,299 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.user.UserService;
import java.util.List;
import java.util.EnumSet;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.FAILURE;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@Slf4j
public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationActionNodeConfiguration> implements TbNode {
protected C config;
private ConcurrentMap<EntityCreationLockKey, Object> entitiesCreationLocks;
private LoadingCache<EntityKey, EntityContainer> entityIdCache;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadEntityNodeActionConfig(configuration);
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
if (this.config.getEntityCacheExpiration() > 0) {
cacheBuilder.expireAfterWrite(this.config.getEntityCacheExpiration(), TimeUnit.SECONDS);
}
entityIdCache = cacheBuilder.build(new EntityCacheLoader(ctx, createEntityIfNotExists()));
}
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
String relationType = processPattern(msg, config.getRelationType());
withCallback(processEntityRelationAction(ctx, msg, relationType),
filterResult -> ctx.tellNext(filterResult.getMsg(), filterResult.isResult() ? SUCCESS : FAILURE), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
protected C config;
@Override
public void destroy() {
if (entityIdCache != null) {
entityIdCache.invalidateAll();
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadEntityNodeActionConfig(configuration);
if (createEntityIfNotExists()) {
entitiesCreationLocks = new ConcurrentReferenceHashMap<>();
}
}
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
}
protected abstract boolean createEntityIfNotExists();
protected abstract ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType);
protected abstract C loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException;
protected ListenableFuture<EntityContainer> getEntity(TbContext ctx, TbMsg msg) {
String entityName = processPattern(msg, this.config.getEntityNamePattern());
String type;
if (this.config.getEntityTypePattern() != null) {
type = processPattern(msg, this.config.getEntityTypePattern());
} else {
type = null;
}
EntityType entityType = EntityType.valueOf(this.config.getEntityType());
EntityKey key = new EntityKey(entityName, type, entityType);
return ctx.getDbCallbackExecutor().executeAsync(() -> {
EntityContainer entityContainer = entityIdCache.get(key);
if (entityContainer.getEntityId() == null) {
throw new RuntimeException("No entity found with type '" + key.getEntityType() + "' and name '" + key.getEntityName() + "'.");
}
return entityContainer;
});
protected String processPattern(TbMsg msg, String pattern) {
return TbNodeUtils.processPattern(pattern, msg);
}
protected SearchDirectionIds processSingleSearchDirection(TbMsg msg, EntityContainer entityContainer) {
SearchDirectionIds searchDirectionIds = new SearchDirectionIds();
if (EntitySearchDirection.FROM.name().equals(this.config.getDirection())) {
searchDirectionIds.setFromId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setToId(msg.getOriginator());
searchDirectionIds.setOriginatorDirectionFrom(false);
} else {
searchDirectionIds.setToId(EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString()));
searchDirectionIds.setFromId(msg.getOriginator());
searchDirectionIds.setOriginatorDirectionFrom(true);
protected ListenableFuture<EntityId> getTargetEntityId(TbContext ctx, TbMsg msg) {
var entityType = config.getEntityType();
var tenantId = ctx.getTenantId();
if (EntityType.TENANT.equals(entityType)) {
return ctx.getDbCallbackExecutor().executeAsync(() -> tenantId);
}
return searchDirectionIds;
}
protected ListenableFuture<List<EntityRelation>> processListSearchDirection(TbContext ctx, TbMsg msg) {
if (EntitySearchDirection.FROM.name().equals(this.config.getDirection())) {
return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), processPattern(msg, this.config.getRelationType()), RelationTypeGroup.COMMON);
} else {
return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), processPattern(msg, this.config.getRelationType()), RelationTypeGroup.COMMON);
var targetEntityName = processPattern(msg, config.getEntityNamePattern());
boolean createEntityIfNotExists = createEntityIfNotExists();
switch (entityType) {
case DEVICE -> {
ListenableFuture<Device> deviceByNameFuture = findDeviceByNameAsync(ctx, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(deviceByNameFuture, device -> {
if (device != null) {
return device.getId();
}
var entityCreationLockKey = new EntityCreationLockKey(tenantId, entityType, targetEntityName);
synchronized (entitiesCreationLocks.computeIfAbsent(entityCreationLockKey, k -> new Object())) {
device = ctx.getDeviceService().findDeviceByTenantIdAndName(tenantId, targetEntityName);
if (device != null) {
return device.getId();
}
var deviceProfileName = processPattern(msg, config.getEntityTypePattern());
var newDevice = new Device();
newDevice.setName(targetEntityName);
newDevice.setType(deviceProfileName);
newDevice.setTenantId(tenantId);
var savedDevice = ctx.getDeviceService().saveDevice(newDevice);
ctx.getClusterService().onDeviceUpdated(savedDevice, null);
ctx.enqueue(ctx.deviceCreatedMsg(savedDevice, ctx.getSelfId()),
() -> log.trace("Pushed Device Created message: {}", savedDevice),
throwable -> log.warn("Failed to push Device Created message: {}", savedDevice, throwable));
return savedDevice.getId();
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(deviceByNameFuture, device -> {
if (device == null) {
throw new NoSuchElementException("Device with name '" + targetEntityName + "' doesn't exist!");
}
return device.getId();
}, MoreExecutors.directExecutor());
}
case ASSET -> {
ListenableFuture<Asset> assetByNameFuture = findAssetByNameAsync(ctx, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(assetByNameFuture, asset -> {
if (asset != null) {
return asset.getId();
}
var entityCreationLockKey = new EntityCreationLockKey(tenantId, entityType, targetEntityName);
synchronized (entitiesCreationLocks.computeIfAbsent(entityCreationLockKey, k -> new Object())) {
asset = ctx.getAssetService().findAssetByTenantIdAndName(tenantId, targetEntityName);
if (asset != null) {
return asset.getId();
}
var assetProfileName = processPattern(msg, config.getEntityTypePattern());
var newAsset = new Asset();
newAsset.setName(targetEntityName);
newAsset.setType(assetProfileName);
newAsset.setTenantId(tenantId);
var savedAsset = ctx.getAssetService().saveAsset(newAsset);
ctx.enqueue(ctx.assetCreatedMsg(savedAsset, ctx.getSelfId()),
() -> log.trace("Pushed Asset Created message: {}", savedAsset),
throwable -> log.warn("Failed to push Asset Created message: {}", savedAsset, throwable));
return savedAsset.getId();
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(assetByNameFuture, asset -> {
if (asset == null) {
throw new NoSuchElementException("Asset with name '" + targetEntityName + "' doesn't exist!");
}
return asset.getId();
}, MoreExecutors.directExecutor());
}
case CUSTOMER -> {
ListenableFuture<Optional<Customer>> customerByTitleFuture = findCustomerByTitleAsync(ctx, targetEntityName);
if (createEntityIfNotExists) {
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
var entityCreationLockKey = new EntityCreationLockKey(tenantId, entityType, targetEntityName);
synchronized (entitiesCreationLocks.computeIfAbsent(entityCreationLockKey, k -> new Object())) {
customerOpt = ctx.getCustomerService().findCustomerByTenantIdAndTitle(tenantId, targetEntityName);
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
}
var newCustomer = new Customer();
newCustomer.setTitle(targetEntityName);
newCustomer.setTenantId(tenantId);
var savedCustomer = ctx.getCustomerService().saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
return savedCustomer.getId();
}
}, MoreExecutors.directExecutor());
}
return Futures.transform(customerByTitleFuture, customerOpt -> {
if (customerOpt.isEmpty()) {
throw new NoSuchElementException("Customer with title '" + targetEntityName + "' doesn't exist!");
}
return customerOpt.get().getId();
}, MoreExecutors.directExecutor());
}
case ENTITY_VIEW -> {
return ctx.getDbCallbackExecutor().executeAsync(() -> {
var entityViewService = ctx.getEntityViewService();
var entityView = entityViewService.findEntityViewByTenantIdAndName(tenantId, targetEntityName);
if (entityView != null) {
return entityView.getId();
}
throw new NoSuchElementException("EntityView with name '" + targetEntityName + "' doesn't exist!");
});
}
case EDGE -> {
return ctx.getDbCallbackExecutor().executeAsync(() -> {
var edgeService = ctx.getEdgeService();
var edge = edgeService.findEdgeByTenantIdAndName(tenantId, targetEntityName);
if (edge != null) {
return edge.getId();
}
throw new NoSuchElementException("Edge with name '" + targetEntityName + "' doesn't exist!");
});
}
case DASHBOARD -> {
return ctx.getDbCallbackExecutor().executeAsync(() -> {
var dashboardService = ctx.getDashboardService();
var dashboardInfo = dashboardService.findFirstDashboardInfoByTenantIdAndName(tenantId, targetEntityName);
if (dashboardInfo != null) {
return dashboardInfo.getId();
}
throw new NoSuchElementException("Dashboard with title '" + targetEntityName + "' doesn't exist!");
});
}
case USER -> {
return ctx.getDbCallbackExecutor().executeAsync(() -> {
var userService = ctx.getUserService();
var user = userService.findUserByTenantIdAndEmail(tenantId, targetEntityName);
if (user != null) {
return user.getId();
}
throw new NoSuchElementException("User with email '" + targetEntityName + "' doesn't exist!");
});
}
default -> throw new IllegalArgumentException(unsupportedEntityTypeErrorMessage(entityType));
}
}
protected String processPattern(TbMsg msg, String pattern) {
return TbNodeUtils.processPattern(pattern, msg);
ListenableFuture<Device> findDeviceByNameAsync(TbContext ctx, String deviceName) {
return ctx.getDbCallbackExecutor().executeAsync(() ->
ctx.getDeviceService().findDeviceByTenantIdAndName(ctx.getTenantId(), deviceName));
}
@Data
@AllArgsConstructor
private static class EntityKey {
private String entityName;
private String type;
private EntityType entityType;
ListenableFuture<Asset> findAssetByNameAsync(TbContext ctx, String assetName) {
return ctx.getDbCallbackExecutor().executeAsync(() ->
ctx.getAssetService().findAssetByTenantIdAndName(ctx.getTenantId(), assetName));
}
@Data
protected static class SearchDirectionIds {
private EntityId fromId;
private EntityId toId;
private boolean originatorDirectionFrom;
ListenableFuture<Optional<Customer>> findCustomerByTitleAsync(TbContext ctx, String customerTitle) {
return ctx.getDbCallbackExecutor().executeAsync(() ->
ctx.getCustomerService().findCustomerByTenantIdAndTitle(ctx.getTenantId(), customerTitle));
}
private static class EntityCacheLoader extends CacheLoader<EntityKey, EntityContainer> {
protected ListenableFuture<Boolean> deleteRelationsByTypeAndDirection(TbContext ctx, TbMsg msg, Executor executor) {
var relationType = processPattern(msg, config.getRelationType());
return deleteRelationsByTypeAndDirection(ctx, msg, relationType, executor);
}
private final TbContext ctx;
private final boolean createIfNotExists;
protected ListenableFuture<Boolean> deleteRelationsByTypeAndDirection(TbContext ctx, TbMsg msg, String relationType, Executor executor) {
var tenantId = ctx.getTenantId();
var originator = msg.getOriginator();
var relationService = ctx.getRelationService();
var originatorRelationsFuture = EntitySearchDirection.FROM.equals(config.getDirection()) ?
relationService.findByFromAndTypeAsync(tenantId, originator, relationType, RelationTypeGroup.COMMON) :
relationService.findByToAndTypeAsync(tenantId, originator, relationType, RelationTypeGroup.COMMON);
return Futures.transformAsync(originatorRelationsFuture, originatorRelations -> {
if (originatorRelations.isEmpty()) {
return Futures.immediateFuture(true);
}
var deleteRelationFutures = originatorRelations.stream()
.map(entityRelation -> relationService.deleteRelationAsync(tenantId, entityRelation))
.collect(Collectors.toList());
return Futures.transform(Futures.allAsList(deleteRelationFutures), deleteResults ->
deleteResults.stream().allMatch(Boolean::booleanValue), executor);
}, executor);
}
private EntityCacheLoader(TbContext ctx, boolean createIfNotExists) {
this.ctx = ctx;
this.createIfNotExists = createIfNotExists;
protected void checkIfConfigEntityTypeIsSupported(EntityType entityType) throws TbNodeException {
if (!supportedEntityTypes.contains(entityType)) {
throw new TbNodeException(unsupportedEntityTypeErrorMessage(entityType), true);
}
}
@Override
public EntityContainer load(EntityKey key) {
return loadEntity(key);
}
private static String unsupportedEntityTypeErrorMessage(EntityType entityType) {
return "Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.";
}
private EntityContainer loadEntity(EntityKey entitykey) {
EntityType type = entitykey.getEntityType();
EntityContainer targetEntity = new EntityContainer();
targetEntity.setEntityType(type);
switch (type) {
case DEVICE:
DeviceService deviceService = ctx.getDeviceService();
Device device = deviceService.findDeviceByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (device != null) {
targetEntity.setEntityId(device.getId());
} else if (createIfNotExists) {
Device newDevice = new Device();
newDevice.setName(entitykey.getEntityName());
newDevice.setType(entitykey.getType());
newDevice.setTenantId(ctx.getTenantId());
Device savedDevice = deviceService.saveDevice(newDevice);
ctx.enqueue(ctx.deviceCreatedMsg(savedDevice, ctx.getSelfId()),
() -> log.trace("Pushed Device Created message: {}", savedDevice),
throwable -> log.warn("Failed to push Device Created message: {}", savedDevice, throwable));
targetEntity.setEntityId(savedDevice.getId());
}
break;
case ASSET:
AssetService assetService = ctx.getAssetService();
Asset asset = assetService.findAssetByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (asset != null) {
targetEntity.setEntityId(asset.getId());
} else if (createIfNotExists) {
Asset newAsset = new Asset();
newAsset.setName(entitykey.getEntityName());
newAsset.setType(entitykey.getType());
newAsset.setTenantId(ctx.getTenantId());
Asset savedAsset = assetService.saveAsset(newAsset);
ctx.enqueue(ctx.assetCreatedMsg(savedAsset, ctx.getSelfId()),
() -> log.trace("Pushed Asset Created message: {}", savedAsset),
throwable -> log.warn("Failed to push Asset Created message: {}", savedAsset, throwable));
targetEntity.setEntityId(savedAsset.getId());
}
break;
case CUSTOMER:
CustomerService customerService = ctx.getCustomerService();
Optional<Customer> customerOptional = customerService.findCustomerByTenantIdAndTitle(ctx.getTenantId(), entitykey.getEntityName());
if (customerOptional.isPresent()) {
targetEntity.setEntityId(customerOptional.get().getId());
} else if (createIfNotExists) {
Customer newCustomer = new Customer();
newCustomer.setTitle(entitykey.getEntityName());
newCustomer.setTenantId(ctx.getTenantId());
Customer savedCustomer = customerService.saveCustomer(newCustomer);
ctx.enqueue(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()),
() -> log.trace("Pushed Customer Created message: {}", savedCustomer),
throwable -> log.warn("Failed to push Customer Created message: {}", savedCustomer, throwable));
targetEntity.setEntityId(savedCustomer.getId());
}
break;
case TENANT:
targetEntity.setEntityId(ctx.getTenantId());
break;
case ENTITY_VIEW:
EntityViewService entityViewService = ctx.getEntityViewService();
EntityView entityView = entityViewService.findEntityViewByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (entityView != null) {
targetEntity.setEntityId(entityView.getId());
}
break;
case EDGE:
EdgeService edgeService = ctx.getEdgeService();
Edge edge = edgeService.findEdgeByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (edge != null) {
targetEntity.setEntityId(edge.getId());
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
boolean hasChanges = false;
var config = (ObjectNode) oldConfiguration;
switch (fromVersion) {
case 0 -> {
if (!config.has("entityCacheExpiration")) {
break;
case DASHBOARD:
DashboardService dashboardService = ctx.getDashboardService();
DashboardInfo dashboardInfo = dashboardService.findFirstDashboardInfoByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
if (dashboardInfo != null) {
targetEntity.setEntityId(dashboardInfo.getId());
}
}
config.remove("entityCacheExpiration");
var directionPropertyName = "direction";
if (!config.has(directionPropertyName)) {
throw new TbNodeException("property to update: '" + directionPropertyName + "' doesn't exists in configuration!");
}
String direction = config.get(directionPropertyName).asText();
if (EntitySearchDirection.TO.name().equals(direction)) {
config.put(directionPropertyName, EntitySearchDirection.FROM.name());
hasChanges = true;
break;
case USER:
UserService userService = ctx.getUserService();
User user = userService.findUserByTenantIdAndEmail(ctx.getTenantId(), entitykey.getEntityName());
if (user != null) {
targetEntity.setEntityId(user.getId());
}
}
if (EntitySearchDirection.FROM.name().equals(direction)) {
config.put(directionPropertyName, EntitySearchDirection.TO.name());
hasChanges = true;
break;
default:
return targetEntity;
}
throw new TbNodeException("property to update: '" + directionPropertyName + "' has invalid value!");
}
return targetEntity;
}
return new TbPair<>(hasChanges, config);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
protected static class RelationContainer {
private TbMsg msg;
private boolean result;
private record EntityCreationLockKey(TenantId tenantId, EntityType entityType, String entityName) {
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNodeConfiguration.java

@ -16,17 +16,17 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
public abstract class TbAbstractRelationActionNodeConfiguration {
private String direction;
private EntitySearchDirection direction;
private String relationType;
private String entityType;
private EntityType entityType;
private String entityNamePattern;
private String entityTypePattern;
private long entityCacheExpiration;
}

80
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java

@ -15,15 +15,16 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -36,12 +37,13 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.ACTION,
name = "assign to customer",
configClazz = TbAssignToCustomerNodeConfiguration.class,
nodeDescription = "Assign Message Originator Entity to Customer",
nodeDetails = "Finds target Customer by customer name pattern and then assign Originator Entity to this customer. " +
"Will create new Customer if it doesn't exists and 'Create new Customer if not exists' is set to true.",
nodeDescription = "Assign message originator entity to customer",
nodeDetails = "Finds target customer by title and assign message originator entity to this customer. " +
"Rule node will create a new customer if it doesn't exist, and 'Create new customer if it doesn't exist' enabled.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeAssignToCustomerConfig",
icon = "add_circle"
icon = "add_circle",
version = 1
)
public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssignToCustomerNodeConfiguration> {
@ -56,53 +58,25 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssig
}
@Override
protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
processAssign(ctx, msg, customerId);
}
private void processAssign(TbContext ctx, TbMsg msg, CustomerId customerId) {
EntityType originatorType = msg.getOriginator().getEntityType();
switch (originatorType) {
case DEVICE:
processAssignDevice(ctx, msg, customerId);
break;
case ASSET:
processAssignAsset(ctx, msg, customerId);
break;
case ENTITY_VIEW:
processAssignEntityView(ctx, msg, customerId);
break;
case EDGE:
processAssignEdge(ctx, msg, customerId);
break;
case DASHBOARD:
processAssignDashboard(ctx, msg, customerId);
break;
default:
ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + originatorType +
"'! Only 'DEVICE', 'ASSET', 'ENTITY_VIEW' or 'DASHBOARD' types are allowed."));
break;
}
}
private void processAssignAsset(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getAssetService().assignAssetToCustomer(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()), customerId);
}
private void processAssignDevice(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDeviceService().assignDeviceToCustomer(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()), customerId);
}
private void processAssignEntityView(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getEntityViewService().assignEntityViewToCustomer(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()), customerId);
}
private void processAssignEdge(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getEdgeService().assignEdgeToCustomer(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId()), customerId);
}
private void processAssignDashboard(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDashboardService().assignDashboardToCustomer(ctx.getTenantId(), new DashboardId(msg.getOriginator().getId()), customerId);
protected ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
var customerIdFuture = getCustomerIdFuture(ctx, msg);
return Futures.transformAsync(customerIdFuture, customerId ->
ctx.getDbCallbackExecutor().submit(() -> {
var originator = msg.getOriginator();
switch (originator.getEntityType()) {
case ASSET ->
ctx.getAssetService().assignAssetToCustomer(ctx.getTenantId(), new AssetId(originator.getId()), customerId);
case DEVICE ->
ctx.getDeviceService().assignDeviceToCustomer(ctx.getTenantId(), new DeviceId(originator.getId()), customerId);
case ENTITY_VIEW ->
ctx.getEntityViewService().assignEntityViewToCustomer(ctx.getTenantId(), new EntityViewId(originator.getId()), customerId);
case EDGE ->
ctx.getEdgeService().assignEdgeToCustomer(ctx.getTenantId(), new EdgeId(originator.getId()), customerId);
case DASHBOARD ->
ctx.getDashboardService().assignDashboardToCustomer(ctx.getTenantId(), new DashboardId(originator.getId()), customerId);
}
return null;
}), MoreExecutors.directExecutor());
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeConfiguration.java

@ -16,19 +16,20 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbAssignToCustomerNodeConfiguration extends TbAbstractCustomerActionNodeConfiguration implements NodeConfiguration<TbAssignToCustomerNodeConfiguration> {
private boolean createCustomerIfNotExists;
@Override
public TbAssignToCustomerNodeConfiguration defaultConfiguration() {
TbAssignToCustomerNodeConfiguration configuration = new TbAssignToCustomerNodeConfiguration();
var configuration = new TbAssignToCustomerNodeConfiguration();
configuration.setCustomerNamePattern("");
configuration.setCreateCustomerIfNotExists(false);
configuration.setCustomerCacheExpiration(300);
return configuration;
}
}

254
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java

@ -17,52 +17,62 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "create relation",
configClazz = TbCreateRelationNodeConfiguration.class,
nodeDescription = "Finds target Entity by entity name pattern and (entity type pattern for Asset, Device) and then create a relation to Originator Entity by type and direction." +
" If Selected entity type: Asset, Device or Customer will create new Entity if it doesn't exist and selected checkbox 'Create new entity if not exists'.<br>" +
" In case that relation from the message originator to the selected entity not exist and If selected checkbox 'Remove current relations'," +
" before creating the new relation all existed relations to message originator by type and direction will be removed.<br>" +
" If relation from the message originator to the selected entity created and If selected checkbox 'Change originator to related entity'," +
" outbound message will be processed as a message from this entity.",
nodeDetails = "If the relation already exists or successfully created - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
nodeDescription = "Finds target entity specified in the configuration and creates a relation with the " +
"incoming message originator based on the configured direction and type.",
nodeDetails = "Useful when you need to create relations between entities dynamically depending on " +
"incoming message payload, message originator type, name, etc.<br><br>" +
"Target entity configuration: " +
"<ul><li><strong>Device</strong> - use a device with the specified name as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable device creation if it doesn't exist. " +
"In advanced mode, device profile name should be specified.</li>" +
"<li><strong>Asset</strong> - use an asset with the specified name as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable device creation if it doesn't exist. " +
"In advanced mode, asset profile name should be specified.</li>" +
"<li><strong>Entity View</strong> - use entity view with the specified name as the target entity to create a relation with.</li>" +
"<li><strong>Tenant</strong> - use current tenant as target entity to create a relation with.</li>" +
"<li><strong>Customer</strong> - use customer with the specified title as the target entity to create a relation with. " +
"When selected, rule node allows us to use advanced mode to enable customer creation if it doesn't exist.</li>" +
"<li><strong>Dashboard</strong> - use a dashboard with the specified title as the target entity to create a relation with.</li>" +
"<li><strong>User</strong> - use a user with the specified email as the target entity to create a relation with.</li>" +
"<li><strong>Edge</strong> - use an edge with the specified name as the target entity to create a relation with.</li></ul>" +
"Advanced settings: " +
"<ul><li><strong>Remove current relations</strong> - removes current relations with originator of the incoming message based on direction and type. " +
"Useful in GPS tracking use cases where relation acts as a temporary indicator of a tracker presence in specific geofence.</li>" +
"<li><strong>Change originator to target entity</strong> - useful when you need to process submitted message as a message from target entity.</li></ul>" +
"Output connections: <code>Success</code> - if the relation already exists or successfully created, otherwise <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCreateRelationConfig",
icon = "add_circle"
icon = "add_circle",
version = 1
)
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {
@Override
protected TbCreateRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
var createRelationNodeConfiguration = TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
checkIfConfigEntityTypeIsSupported(createRelationNodeConfiguration.getEntityType());
return createRelationNodeConfiguration;
}
@Override
@ -71,167 +81,59 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateR
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity, String relationType) {
ListenableFuture<Boolean> future = createRelationIfAbsent(ctx, msg, entity, relationType);
return Futures.transform(future, result -> {
if (result && config.isChangeOriginatorToRelatedEntity()) {
TbMsg tbMsg = ctx.transformMsgOriginator(msg, entity.getEntityId());
return new RelationContainer(tbMsg, result);
public void onMsg(TbContext ctx, TbMsg msg) {
var targetEntityIdFuture = getTargetEntityId(ctx, msg);
var createRelationResultFuture = Futures.transformAsync(targetEntityIdFuture, targetEntityId -> {
var originator = msg.getOriginator();
var relationType = processPattern(msg, config.getRelationType());
if (config.isRemoveCurrentRelations()) {
var removalOfCurrentRelationsFuture = deleteRelationsByTypeAndDirection(ctx, msg, relationType, MoreExecutors.directExecutor());
return Futures.transformAsync(removalOfCurrentRelationsFuture, __ ->
checkRelationAndCreateIfAbsent(ctx, originator, targetEntityId, relationType), MoreExecutors.directExecutor());
}
return new RelationContainer(msg, result);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> createRelationIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
return Futures.transformAsync(deleteCurrentRelationsIfNeeded(ctx, msg, sdId, relationType), v ->
checkRelationAndCreateIfAbsent(ctx, entityContainer, relationType, sdId),
ctx.getDbCallbackExecutor());
}
private ListenableFuture<Void> deleteCurrentRelationsIfNeeded(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) {
if (config.isRemoveCurrentRelations()) {
return deleteOriginatorRelations(ctx, findOriginatorRelations(ctx, msg, sdId, relationType));
return checkRelationAndCreateIfAbsent(ctx, originator, targetEntityId, relationType);
}, MoreExecutors.directExecutor());
if (!config.isChangeOriginatorToRelatedEntity()) {
withCallback(createRelationResultFuture,
relationCreated -> {
if (relationCreated) {
ctx.tellSuccess(msg);
return;
}
ctx.tellFailure(msg, new RuntimeException("Failed to create originator relation with target entity!"));
},
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
return;
}
return Futures.immediateFuture(null);
}
private ListenableFuture<List<EntityRelation>> findOriginatorRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) {
if (sdId.isOriginatorDirectionFrom()) {
return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
} else {
return ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON);
}
}
private ListenableFuture<Void> deleteOriginatorRelations(TbContext ctx, ListenableFuture<List<EntityRelation>> originatorRelationsFuture) {
return Futures.transformAsync(originatorRelationsFuture, originatorRelations -> {
List<ListenableFuture<Boolean>> list = new ArrayList<>();
if (!CollectionUtils.isEmpty(originatorRelations)) {
for (EntityRelation relation : originatorRelations) {
list.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation));
}
}
return Futures.transform(Futures.allAsList(list), result -> null, ctx.getDbCallbackExecutor());
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> checkRelationAndCreateIfAbsent(TbContext ctx, EntityContainer entityContainer, String relationType, SearchDirectionIds sdId) {
return Futures.transformAsync(checkRelation(ctx, sdId, relationType), relationPresent -> {
if (relationPresent) {
return Futures.immediateFuture(true);
}
return processCreateRelation(ctx, entityContainer, sdId, relationType);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> checkRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON);
}
private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
switch (entityContainer.getEntityType()) {
case ASSET:
return processAsset(ctx, entityContainer, sdId, relationType);
case DEVICE:
return processDevice(ctx, entityContainer, sdId, relationType);
case CUSTOMER:
return processCustomer(ctx, entityContainer, sdId, relationType);
case DASHBOARD:
return processDashboard(ctx, entityContainer, sdId, relationType);
case ENTITY_VIEW:
return processView(ctx, entityContainer, sdId, relationType);
case EDGE:
return processEdge(ctx, entityContainer, sdId, relationType);
case TENANT:
return processTenant(ctx, entityContainer, sdId, relationType);
case USER:
return processUser(ctx, entityContainer, sdId, relationType);
}
return Futures.immediateFuture(true);
}
private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
if (entityView != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
withCallback(Futures.allAsList(targetEntityIdFuture, createRelationResultFuture), result -> {
var targetEntityId = (EntityId) result.get(0);
var relationCreated = (Boolean) result.get(1);
if (relationCreated) {
var transformedMsg = ctx.transformMsgOriginator(msg, targetEntityId);
ctx.tellSuccess(transformedMsg);
return;
}
}, ctx.getDbCallbackExecutor());
ctx.tellFailure(msg, new RuntimeException("Failed to create originator relation with target entity!"));
}, t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<Boolean> processEdge(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(entityContainer.getEntityId().getId())), edge -> {
if (edge != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId()));
if (device != null) {
return processSave(ctx, sdId, relationType);
private ListenableFuture<Boolean> checkRelationAndCreateIfAbsent(TbContext ctx, EntityId originator, EntityId targetEntityId, String relationType) {
EntityId fromId;
EntityId toId;
if (EntitySearchDirection.FROM.equals(config.getDirection())) {
fromId = originator;
toId = targetEntityId;
} else {
return Futures.immediateFuture(true);
toId = originator;
fromId = targetEntityId;
}
}
private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
if (asset != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
if (customer != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
if (dashboard != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), TenantId.fromUUID(entityContainer.getEntityId().getId())), tenant -> {
if (tenant != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processUser(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) {
return Futures.transformAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), new UserId(entityContainer.getEntityId().getId())), user -> {
if (user != null) {
return processSave(ctx, sdId, relationType);
} else {
return Futures.immediateFuture(true);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSave(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON));
var checkRelationFuture = ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), fromId, toId, relationType, RelationTypeGroup.COMMON);
return Futures.transformAsync(checkRelationFuture, relationExists ->
relationExists ?
Futures.immediateFuture(true) :
ctx.getRelationService().
saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, relationType, RelationTypeGroup.COMMON)),
MoreExecutors.directExecutor());
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeConfiguration.java

@ -16,10 +16,13 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbCreateRelationNodeConfiguration> {
private boolean createEntityIfNotExists;
@ -29,10 +32,9 @@ public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionN
@Override
public TbCreateRelationNodeConfiguration defaultConfiguration() {
TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType("Contains");
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityNamePattern("");
configuration.setEntityCacheExpiration(300);
configuration.setCreateEntityIfNotExists(false);
configuration.setRemoveCurrentRelations(false);
configuration.setChangeOriginatorToRelatedEntity(false);

110
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java

@ -17,20 +17,20 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntityContainer;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@ -38,18 +38,36 @@ import java.util.List;
type = ComponentType.ACTION,
name = "delete relation",
configClazz = TbDeleteRelationNodeConfiguration.class,
nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction" +
" if 'Delete single entity' is set to true, otherwise rule node will delete all relations to the originator of the message by type and direction.",
nodeDetails = "If the relation(s) successfully deleted - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
nodeDescription = "Deletes relation with the incoming message originator based on the configured direction and type.",
nodeDetails = "Useful when you need to remove relations between entities dynamically depending on incoming message payload, " +
"message originator type, name, etc.<br><br>" +
"If <strong>Delete relation with specific entity</strong> enabled, target entity to delete relation with should be specified. " +
"Otherwise, rule node will delete all relations with the message originator based on the configured direction and type.<br><br>" +
"Target entity configuration: " +
"<ul><li><strong>Device</strong> - use a device with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Asset</strong> - use an asset with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Entity View</strong> - use entity view with the specified name as the target entity to delete relation with.</li>" +
"<li><strong>Tenant</strong> - use current tenant as target entity to delete relation with.</li>" +
"<li><strong>Customer</strong> - use customer with the specified title as the target entity to delete relation with.</li>" +
"<li><strong>Dashboard</strong> - use a dashboard with the specified title as the target entity to delete relation with.</li>" +
"<li><strong>User</strong> - use a user with the specified email as the target entity to delete relation with.</li>" +
"<li><strong>Edge</strong> - use an edge with the specified name as the target entity to delete relation with.</li></ul>" +
"Output connections: <code>Success</code> - If the relation(s) successfully deleted, otherwise <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeDeleteRelationConfig",
icon = "remove_circle"
icon = "remove_circle",
version = 1
)
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
@Override
protected TbDeleteRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
var deleteRelationNodeConfiguration = TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
if (!deleteRelationNodeConfiguration.isDeleteForSingleEntity()) {
return deleteRelationNodeConfiguration;
}
checkIfConfigEntityTypeIsSupported(deleteRelationNodeConfiguration.getEntityType());
return deleteRelationNodeConfiguration;
}
@Override
@ -58,57 +76,41 @@ public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteR
}
@Override
protected ListenableFuture<RelationContainer> processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) {
return getRelationContainerListenableFuture(ctx, msg, relationType);
}
@Override
protected ListenableFuture<RelationContainer> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
return Futures.transform(processSingle(ctx, msg, entityContainer, relationType), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
public void onMsg(TbContext ctx, TbMsg msg) {
ListenableFuture<Boolean> deleteResultFuture = config.isDeleteForSingleEntity() ?
Futures.transformAsync(getTargetEntityId(ctx, msg), targetEntityId ->
deleteRelationToSpecificEntity(ctx, msg, targetEntityId), MoreExecutors.directExecutor()) :
deleteRelationsByTypeAndDirection(ctx, msg, ctx.getDbCallbackExecutor());
withCallback(deleteResultFuture, deleted -> {
if (deleted) {
ctx.tellSuccess(msg);
return;
}
ctx.tellFailure(msg, new RuntimeException("Failed to delete relation(s) with originator!"));
},
t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<RelationContainer> getRelationContainerListenableFuture(TbContext ctx, TbMsg msg, String relationType) {
if (config.isDeleteForSingleEntity()) {
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer, relationType), ctx.getDbCallbackExecutor());
private ListenableFuture<Boolean> deleteRelationToSpecificEntity(TbContext ctx, TbMsg msg, EntityId targetEntityId) {
EntityId fromId;
EntityId toId;
if (EntitySearchDirection.FROM.equals(config.getDirection())) {
fromId = msg.getOriginator();
toId = targetEntityId;
} else {
return Futures.transform(processList(ctx, msg), result -> new RelationContainer(msg, result), ctx.getDbCallbackExecutor());
toId = msg.getOriginator();
fromId = targetEntityId;
}
}
private ListenableFuture<Boolean> processList(TbContext ctx, TbMsg msg) {
return Futures.transformAsync(processListSearchDirection(ctx, msg), entityRelations -> {
if (entityRelations.isEmpty()) {
return Futures.immediateFuture(true);
} else {
List<ListenableFuture<Boolean>> listenableFutureList = new ArrayList<>();
for (EntityRelation entityRelation : entityRelations) {
listenableFutureList.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), entityRelation));
}
return Futures.transformAsync(Futures.allAsList(listenableFutureList), booleans -> {
for (Boolean bool : booleans) {
if (!bool) {
return Futures.immediateFuture(false);
}
var relationType = processPattern(msg, config.getRelationType());
var tenantId = ctx.getTenantId();
var relationService = ctx.getRelationService();
return Futures.transformAsync(relationService.checkRelationAsync(tenantId, fromId, toId, relationType, RelationTypeGroup.COMMON),
relationExists -> {
if (relationExists) {
return relationService.deleteRelationAsync(tenantId, fromId, toId, relationType, RelationTypeGroup.COMMON);
}
return Futures.immediateFuture(true);
}, ctx.getDbCallbackExecutor());
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingle(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) {
SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer);
return Futures.transformAsync(ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON),
result -> {
if (result) {
return processSingleDeleteRelation(ctx, sdId, relationType);
}
return Futures.immediateFuture(true);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Boolean> processSingleDeleteRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) {
return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON);
}, MoreExecutors.directExecutor());
}
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeConfiguration.java

@ -16,10 +16,13 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbDeleteRelationNodeConfiguration> {
private boolean deleteForSingleEntity;
@ -27,11 +30,10 @@ public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionN
@Override
public TbDeleteRelationNodeConfiguration defaultConfiguration() {
TbDeleteRelationNodeConfiguration configuration = new TbDeleteRelationNodeConfiguration();
configuration.setDeleteForSingleEntity(true);
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType("Contains");
configuration.setDeleteForSingleEntity(false);
configuration.setDirection(EntitySearchDirection.FROM);
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityNamePattern("");
configuration.setEntityCacheExpiration(300);
return configuration;
}
}

82
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java

@ -15,14 +15,17 @@
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -34,11 +37,15 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.ACTION,
name = "unassign from customer",
configClazz = TbUnassignFromCustomerNodeConfiguration.class,
nodeDescription = "Unassign Message Originator Entity from Customer",
nodeDetails = "Finds target Entity Customer by Customer name pattern and then unassign Originator Entity from this customer.",
nodeDescription = "Unassign message originator entity from customer",
nodeDetails = "If the message originator is not assigned to any customer, rule node will do nothing. <br><br>" +
"If the incoming message originator is a dashboard, will try to search for the customer by title specified in the configuration. " +
"If customer doesn't exist, the exception will be thrown. Otherwise will unassign the dashboard from retrieved customer.<br><br>" +
"Other entities can be assigned only to one customer, so specified customer title in the configuration will be ignored if the originator isn't a dashboard.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeUnAssignToCustomerConfig",
icon = "remove_circle"
icon = "remove_circle",
version = 1
)
public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode<TbUnassignFromCustomerNodeConfiguration> {
@ -53,48 +60,33 @@ public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode<TbU
}
@Override
protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
EntityType originatorType = msg.getOriginator().getEntityType();
switch (originatorType) {
case DEVICE:
processUnnasignDevice(ctx, msg);
break;
case ASSET:
processUnnasignAsset(ctx, msg);
break;
case ENTITY_VIEW:
processUnassignEntityView(ctx, msg);
break;
case EDGE:
processUnassignEdge(ctx, msg);
break;
case DASHBOARD:
processUnnasignDashboard(ctx, msg, customerId);
break;
default:
ctx.tellFailure(msg, new RuntimeException("Unsupported originator type '" + originatorType +
"'! Only 'DEVICE', 'ASSET', 'ENTITY_VIEW' or 'DASHBOARD' types are allowed."));
break;
protected ListenableFuture<Void> processCustomerAction(TbContext ctx, TbMsg msg) {
var originator = msg.getOriginator();
var originatorType = originator.getEntityType();
var tenantId = ctx.getTenantId();
if (EntityType.DASHBOARD.equals(originatorType)) {
if (StringUtils.isEmpty(config.getCustomerNamePattern())) {
throw new RuntimeException("Failed to unassign dashboard with id '" +
originator.getId() + "' from customer! Customer title should be specified!");
}
var customerIdFuture = getCustomerIdFuture(ctx, msg);
return Futures.transformAsync(customerIdFuture, customerId -> {
ctx.getDashboardService().unassignDashboardFromCustomer(tenantId, new DashboardId(originator.getId()), customerId);
return Futures.immediateFuture(null);
}, MoreExecutors.directExecutor());
}
return ctx.getDbCallbackExecutor().submit(() -> {
switch (originatorType) {
case ASSET ->
ctx.getAssetService().unassignAssetFromCustomer(tenantId, new AssetId(originator.getId()));
case DEVICE ->
ctx.getDeviceService().unassignDeviceFromCustomer(tenantId, new DeviceId(originator.getId()));
case ENTITY_VIEW ->
ctx.getEntityViewService().unassignEntityViewFromCustomer(tenantId, new EntityViewId(originator.getId()));
case EDGE -> ctx.getEdgeService().unassignEdgeFromCustomer(tenantId, new EdgeId(originator.getId()));
}
return null;
});
}
private void processUnnasignAsset(TbContext ctx, TbMsg msg) {
ctx.getAssetService().unassignAssetFromCustomer(ctx.getTenantId(), new AssetId(msg.getOriginator().getId()));
}
private void processUnnasignDevice(TbContext ctx, TbMsg msg) {
ctx.getDeviceService().unassignDeviceFromCustomer(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId()));
}
private void processUnnasignDashboard(TbContext ctx, TbMsg msg, CustomerId customerId) {
ctx.getDashboardService().unassignDashboardFromCustomer(ctx.getTenantId(), new DashboardId(msg.getOriginator().getId()), customerId);
}
private void processUnassignEntityView(TbContext ctx, TbMsg msg) {
ctx.getEntityViewService().unassignEntityViewFromCustomer(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId()));
}
private void processUnassignEdge(TbContext ctx, TbMsg msg) {
ctx.getEdgeService().unassignEdgeFromCustomer(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId()));
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeConfiguration.java

@ -16,16 +16,17 @@
package org.thingsboard.rule.engine.action;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbUnassignFromCustomerNodeConfiguration extends TbAbstractCustomerActionNodeConfiguration implements NodeConfiguration<TbUnassignFromCustomerNodeConfiguration> {
@Override
public TbUnassignFromCustomerNodeConfiguration defaultConfiguration() {
TbUnassignFromCustomerNodeConfiguration configuration = new TbUnassignFromCustomerNodeConfiguration();
var configuration = new TbUnassignFromCustomerNodeConfiguration();
configuration.setCustomerNamePattern("");
configuration.setCustomerCacheExpiration(300);
return configuration;
}
}

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java

@ -60,8 +60,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
configDirective = "tbFilterNodeCheckRelationConfig")
public class TbCheckRelationNode implements TbNode {
private static final String DIRECTION_PROPERTY_NAME = "direction";
private TbCheckRelationNodeConfiguration config;
private EntityId singleEntityId;
@ -114,19 +112,20 @@ public class TbCheckRelationNode implements TbNode {
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
if (fromVersion == 0) {
var newConfigObjectNode = (ObjectNode) oldConfiguration;
if (!newConfigObjectNode.has(DIRECTION_PROPERTY_NAME)) {
throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' doesn't exists in configuration!");
var directionPropertyName = "direction";
if (!newConfigObjectNode.has(directionPropertyName)) {
throw new TbNodeException("property to update: '" + directionPropertyName + "' doesn't exists in configuration!");
}
String direction = newConfigObjectNode.get(DIRECTION_PROPERTY_NAME).asText();
String direction = newConfigObjectNode.get(directionPropertyName).asText();
if (EntitySearchDirection.TO.name().equals(direction)) {
newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.FROM.name());
newConfigObjectNode.put(directionPropertyName, EntitySearchDirection.FROM.name());
return new TbPair<>(true, newConfigObjectNode);
}
if (EntitySearchDirection.FROM.name().equals(direction)) {
newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.TO.name());
newConfigObjectNode.put(directionPropertyName, EntitySearchDirection.TO.name());
return new TbPair<>(true, newConfigObjectNode);
}
throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' has invalid value!");
throw new TbNodeException("property to update: '" + directionPropertyName + "' has invalid value!");
}
return new TbPair<>(false, oldConfiguration);
}

335
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNodeTest.java

@ -0,0 +1,335 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
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.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TbAssignToCustomerNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET,
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private final Device DEVICE = new Device();
private final Asset ASSET = new Asset();
private final EntityView ENTITY_VIEW = new EntityView();
private final Edge EDGE = new Edge();
private final Dashboard DASHBOARD = new Dashboard();
private final TenantId TENANT_ID = new TenantId(UUID.fromString("c818385f-e661-407f-8c52-daf2dddf406d"));
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("c3570bd0-c0bc-4609-97a4-6f57d7c8b809"));
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() {
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type)));
}
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify() {
return supportedEntityTypes.stream()
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5))));
}
private TbAssignToCustomerNode node;
private TbAssignToCustomerNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbAssignToCustomerNode());
config = new TbAssignToCustomerNodeConfiguration().defaultConfiguration();
}
@Override
protected TbNode getTestNode() {
return node;
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbAssignToCustomerNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty();
assertThat(defaultConfig.isCreateCustomerIfNotExists()).isFalse();
}
@ParameterizedTest
@MethodSource
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) {
// GIVEN
var originator = toOriginator(originatorType);
var msg = getTbMsg(originator);
// WHEN
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
verifyNoInteractions(ctxMock);
verifyNoInteractions(customerServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.of(customer));
Map<EntityType, Consumer<EntityId>> entityTypeToAssignConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToAssignConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerCreatedAndSuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(ctxMock.getSelfId()).thenReturn(RULE_NODE_ID);
config.setCreateCustomerIfNotExists(true);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty());
when(customerServiceMock.saveCustomer(any(Customer.class))).thenReturn(customer);
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(ctxMock).enqueue(any(), runnableCaptor.capture(), any());
runnableCaptor.getValue().run();
verify(ctxMock).customerCreatedMsg(any(), eq(RULE_NODE_ID));
verifyMsgSuccess(msg);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerNotFound(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty());
// WHEN
node.onMsg(ctxMock, msg);
// THEN
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!");
verifyNoMoreInteractions(customerServiceMock);
verifyNoMoreInteractions(ctxMock);
}
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() {
return Map.of(
EntityType.DEVICE, id -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.assignDeviceToCustomer(eq(TENANT_ID), (DeviceId) eq(id), any()))
.thenReturn(DEVICE);
},
EntityType.ASSET, id -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.assignAssetToCustomer(eq(TENANT_ID), (AssetId) eq(id), any()))
.thenReturn(ASSET);
},
EntityType.ENTITY_VIEW, id -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.assignEntityViewToCustomer(eq(TENANT_ID), (EntityViewId) eq(id), any()))
.thenReturn(ENTITY_VIEW);
},
EntityType.EDGE, id -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.assignEdgeToCustomer(eq(TENANT_ID), (EdgeId) eq(id), any()))
.thenReturn(EDGE);
},
EntityType.DASHBOARD, id -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.assignDashboardToCustomer(eq(TENANT_ID), (DashboardId) eq(id), any()))
.thenReturn(DASHBOARD);
}
);
}
private void verifyMsgSuccess(TbMsg expectedMsg) {
verify(ctxMock).tellSuccess(eq(expectedMsg));
verify(ctxMock, never()).tellFailure(any(), any());
}
private Customer createCustomer(String customerTitle) {
var customer = new Customer();
customer.setTitle(customerTitle);
customer.setId(new CustomerId(UUID.randomUUID()));
customer.setTenantId(TENANT_ID);
return customer;
}
private TbMsg getTbMsg(EntityId originator) {
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
}
private EntityId toOriginator(EntityType type) {
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString());
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\",\"customerCacheExpiration\":300}",
true,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}",
false,
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}")
);
}
}

747
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java

@ -15,193 +15,708 @@
*/
package org.thingsboard.rule.engine.action;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
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.RuleChainId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
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.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import static org.junit.Assert.assertEquals;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TbCreateRelationNodeTest {
@ExtendWith(MockitoExtension.class)
public class TbCreateRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private TbCreateRelationNode node;
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown() {
return supportedEntityTypes.stream().filter(entityType -> !entityType.equals(EntityType.TENANT)).map(Arguments::of);
}
private static final TenantId tenantId = new TenantId(UUID.fromString("6fc86fc9-b25c-4893-b340-51cf4e101ab2"));
private static final DeviceId deviceId = new DeviceId(UUID.fromString("191ab124-1d8d-4749-97c6-fc84c113c1f5"));
private static final AssetId assetId = new AssetId(UUID.fromString("a47a5867-deab-4333-b845-88cb1695990c"));
private static final CustomerId customerId = new CustomerId(UUID.fromString("4af69229-273d-40de-9fba-f49f87373d23"));
private static final EntityViewId entityViewId = new EntityViewId(UUID.fromString("d4c22c9c-07f5-474d-9d16-b63f0e71f914"));
private static final EdgeId edgeId = new EdgeId(UUID.fromString("7c653959-558d-4661-aac7-c1866eef286b"));
private static final DashboardId dashboardId = new DashboardId(UUID.fromString("6fcfbcb0-21e4-4b0b-a0d6-399ca6959cb2"));
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId)),
Arguments.of(new EntityView(entityViewId)),
Arguments.of(new Edge(edgeId)),
Arguments.of(new Dashboard(dashboardId)),
Arguments.of(new Tenant(tenantId))
);
}
private static Stream<Arguments> givenSupportedEntityTypeToCreateEntityIfNotExists_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId))
);
}
private final DeviceId originatorId = new DeviceId(UUID.fromString("860634b1-8a1e-4693-9ae8-e779c7f5f4da"));
private final RuleNodeId ruleNodeId = new RuleNodeId(UUID.fromString("d05a0491-ee7a-484a-8c1b-91111ef39287"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private TbContext ctx;
private CustomerService customerServiceMock;
@Mock
private AssetService assetService;
private EdgeService edgeServiceMock;
@Mock
private RelationService relationService;
private UserService userServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@Mock
private TbClusterService clusterServiceMock;
@Mock
private RelationService relationServiceMock;
private TbMsg msg;
private TbCreateRelationNode node;
private TbCreateRelationNodeConfiguration config;
private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbCreateRelationNode());
config = new TbCreateRelationNodeConfiguration().defaultConfiguration();
}
private ListeningExecutor dbExecutor;
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbCreateRelationNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getDirection()).isEqualTo(EntitySearchDirection.FROM);
assertThat(defaultConfig.getRelationType()).isEqualTo(EntityRelation.CONTAINS_TYPE);
assertThat(defaultConfig.getEntityNamePattern()).isEqualTo("");
assertThat(defaultConfig.getEntityTypePattern()).isEqualTo(null);
assertThat(defaultConfig.getEntityType()).isEqualTo(null);
assertThat(defaultConfig.isCreateEntityIfNotExists()).isFalse();
assertThat(defaultConfig.isRemoveCurrentRelations()).isFalse();
assertThat(defaultConfig.isChangeOriginatorToRelatedEntity()).isFalse();
}
@Before
public void before() {
dbExecutor = new TestDbCallbackExecutor();
@ParameterizedTest
@EnumSource(EntityType.class)
void givenEntityType_whenInit_thenVerifyExceptionThrownIfTypeIsUnsupported(EntityType entityType) {
// GIVEN
config.setEntityType(entityType);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN-THEN
if (unsupportedEntityTypes.contains(entityType)) {
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
} else {
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)).doesNotThrowAnyException();
}
verifyNoInteractions(ctxMock);
}
@Test
public void testCreateNewRelation() throws TbNodeException {
init(createRelationNodeConfig());
@ParameterizedTest
@MethodSource
void givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown(EntityType entityType) throws TbNodeException {
// GIVEN
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var mockMethodCallsMap = mockEntityServiceCallsEntityNotFound();
mockMethodCallsMap.get(entityType).run();
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// todo fix TestDbCallbackExecutor exception handling.
switch (entityType) {
case CUSTOMER -> {
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage(EntityType.CUSTOMER.getNormalName() + " with title 'EntityName' doesn't exist!");
}
case DEVICE, ASSET -> {
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage(entityType.getNormalName() + " with name 'EntityName' doesn't exist!");
}
default -> assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(RuntimeException.class).hasCauseInstanceOf(NoSuchElementException.class);
}
}
DeviceId deviceId = new DeviceId(Uuids.timeBased());
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationCreatedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(false);
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
public void testDeleteCurrentRelationsCreateNewRelation() throws TbNodeException {
init(createRelationNodeConfigWithRemoveCurrentRelations());
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyDeleteCurrentRelationCreateNewRelationAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyDeleteCurrentRelationsCreateNewRelationAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
DeviceId deviceId = new DeviceId(Uuids.timeBased());
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(true);
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
EntityRelation relation = new EntityRelation();
when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(Collections.singletonList(relation)));
when(ctx.getRelationService().deleteRelationAsync(any(), eq(relation))).thenReturn(Futures.immediateFuture(true));
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
var firstRelationToDelete = new EntityRelation();
var secondRelationToDelete = new EntityRelation();
var relationsToDelete = List.of(firstRelationToDelete, secondRelationToDelete);
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS);
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(relationsToDelete));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
var entityRelationCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationServiceMock, times(2)).deleteRelationAsync(eq(tenantId), entityRelationCaptor.capture());
assertThat(relationsToDelete).containsExactlyInAnyOrderElementsOf(relationsToDelete);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
public void testCreateNewRelationAndChangeOriginator() throws TbNodeException {
init(createRelationNodeConfigWithChangeOriginator());
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationCreatedAndOriginatorChanged(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(false);
config.setChangeOriginatorToRelatedEntity(true);
config.setRemoveCurrentRelations(false);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsDisabled();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
var msgAfterOriginatorChanged = TbMsg.transformMsgOriginator(msg, originatorId);
when(ctxMock.transformMsgOriginator(any(), any())).thenReturn(msgAfterOriginatorChanged);
// WHEN
node.onMsg(ctxMock, msg);
DeviceId deviceId = new DeviceId(Uuids.timeBased());
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsDisabled();
verifyMethodCallsMap.get(entityType).run();
AssetId assetId = new AssetId(Uuids.timeBased());
Asset asset = new Asset();
asset.setId(assetId);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset));
verify(ctxMock).transformMsgOriginator(eq(msg), eq(entityId));
verify(ctxMock).tellSuccess(eq(msgAfterOriginatorChanged));
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityTypeToCreateEntityIfNotExists_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsg_thenVerifyRelationAndEntityCreatedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setCreateEntityIfNotExists(true);
config.setChangeOriginatorToRelatedEntity(false);
config.setRemoveCurrentRelations(false);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getSelfId()).thenReturn(ruleNodeId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("name", "AssetName");
metaData.putValue("type", "AssetType");
msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId);
var mockMethodCallsMap = mockEntityServiceCallsCreateEntityIfNotExistsEnabled();
var entityCreatedMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
mockMethodCallsMap.get(entityType).accept(entity, entityCreatedMsg);
when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)))
.thenReturn(Futures.immediateFuture(false));
when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON))))
.thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
when(relationServiceMock.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture());
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
assertEquals(assetId, originatorCaptor.getValue());
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCallsCreateEntityIfNotExistsEnabled();
verifyMethodCallsMap.get(entityType).accept(entity, entityCreatedMsg);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).saveRelationAsync(eq(tenantId), eq(new EntityRelation(originatorId, entityId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
public void init(TbCreateRelationNodeConfiguration configuration) throws TbNodeException {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(configuration));
private Map<EntityType, Consumer<HasId>> mockEntityServiceCallsCreateEntityIfNotExistsDisabled() {
return Map.of(
EntityType.DEVICE, hasId -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
},
EntityType.ASSET, hasId -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(asset);
},
EntityType.CUSTOMER, hasId -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.ofNullable(customer));
},
EntityType.ENTITY_VIEW, hasId -> {
var entityView = (EntityView) hasId;
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(entityView);
},
EntityType.EDGE, hasId -> {
var edge = (Edge) hasId;
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(edge);
},
EntityType.USER, hasId -> {
var user = (User) hasId;
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(user);
},
EntityType.DASHBOARD, hasId -> {
var dashboard = (Dashboard) hasId;
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(dashboard);
},
EntityType.TENANT, hasId -> {
}
);
}
when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctx.getRelationService()).thenReturn(relationService);
when(ctx.getAssetService()).thenReturn(assetService);
private Map<EntityType, Runnable> verifyEntityServiceCallsCreateEntityIfNotExistsDisabled() {
return Map.of(
EntityType.DEVICE, () -> {
verify(deviceServiceMock).findDeviceByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(deviceServiceMock);
},
EntityType.ASSET, () -> {
verify(assetServiceMock).findAssetByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, () -> {
verify(customerServiceMock).findCustomerByTenantIdAndTitle(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(customerServiceMock);
},
EntityType.ENTITY_VIEW, () -> {
verify(entityViewServiceMock).findEntityViewByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(entityViewServiceMock);
},
EntityType.EDGE, () -> {
verify(edgeServiceMock).findEdgeByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(edgeServiceMock);
},
EntityType.USER, () -> {
verify(userServiceMock).findUserByTenantIdAndEmail(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(userServiceMock);
},
EntityType.DASHBOARD, () -> {
verify(dashboardServiceMock).findFirstDashboardInfoByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(dashboardServiceMock);
},
EntityType.TENANT, () -> {
}
);
}
private Map<EntityType, BiConsumer<HasId, TbMsg>> mockEntityServiceCallsCreateEntityIfNotExistsEnabled() {
return Map.of(
EntityType.DEVICE, (hasId, entityCreatedMsg) -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getClusterService()).thenReturn(clusterServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(null);
when(deviceServiceMock.saveDevice(any())).thenReturn(device);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).deviceCreatedMsg(any(), any());
},
EntityType.ASSET, (hasId, entityCreatedMsg) -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(null);
when(assetServiceMock.saveAsset(any())).thenReturn(asset);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).assetCreatedMsg(any(), any());
},
EntityType.CUSTOMER, (hasId, entityCreatedMsg) -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.empty());
when(customerServiceMock.saveCustomer(any())).thenReturn(customer);
doAnswer(invocation -> entityCreatedMsg).when(ctxMock).customerCreatedMsg(any(), any());
}
);
}
private Map<EntityType, BiConsumer<HasId, TbMsg>> verifyEntityServiceCallsCreateEntityIfNotExistsEnabled() {
return Map.of(
EntityType.DEVICE, (hasId, entityCreatedMsg) -> {
var device = (Device) hasId;
verify(deviceServiceMock, times(2)).findDeviceByTenantIdAndName(eq(tenantId), eq("EntityName"));
verify(deviceServiceMock).saveDevice(any());
verify(clusterServiceMock).onDeviceUpdated(eq(device), eq(null));
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(deviceServiceMock, clusterServiceMock);
},
EntityType.ASSET, (hasId, entityCreatedMsg) -> {
verify(assetServiceMock, times(2)).findAssetByTenantIdAndName(eq(tenantId), eq("EntityName"));
verify(assetServiceMock).saveAsset(any());
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, (hasId, entityCreatedMsg) -> {
verify(customerServiceMock, times(2)).findCustomerByTenantIdAndTitle(eq(tenantId), eq("EntityName"));
verify(customerServiceMock).saveCustomer(any());
verify(ctxMock).enqueue(eq(entityCreatedMsg), any(), any());
verifyNoMoreInteractions(customerServiceMock);
}
);
}
private Map<EntityType, Runnable> mockEntityServiceCallsEntityNotFound() {
return Map.of(
EntityType.DEVICE, () -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.ASSET, () -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.CUSTOMER, () -> {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.empty());
},
EntityType.ENTITY_VIEW, () -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.EDGE, () -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.USER, () -> {
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(null);
},
EntityType.DASHBOARD, () -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(null);
}
);
}
node = new TbCreateRelationNode();
node.init(ctx, nodeConfiguration);
private TbMsg getTbMsg(EntityId originator, TbMsgMetaData metaData) {
return TbMsg.newMsg(TbMsgType.NA, originator, metaData, TbMsg.EMPTY_JSON_OBJECT);
}
private TbCreateRelationNodeConfiguration createRelationNodeConfig() {
TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
configuration.setDirection(EntitySearchDirection.FROM.name());
configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
configuration.setEntityCacheExpiration(300);
configuration.setEntityType(EntityType.ASSET.name());
configuration.setEntityNamePattern("${name}");
configuration.setEntityTypePattern("${type}");
configuration.setCreateEntityIfNotExists(false);
configuration.setChangeOriginatorToRelatedEntity(false);
configuration.setRemoveCurrentRelations(false);
return configuration;
private TbMsgMetaData getMetadataWithNameTemplate() {
var metaData = new TbMsgMetaData();
metaData.putValue("name", "EntityName");
return metaData;
}
private TbCreateRelationNodeConfiguration createRelationNodeConfigWithRemoveCurrentRelations() {
TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig();
configuration.setRemoveCurrentRelations(true);
return configuration;
@Override
protected TbNode getTestNode() {
return node;
}
private TbCreateRelationNodeConfiguration createRelationNodeConfigWithChangeOriginator() {
TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig();
configuration.setChangeOriginatorToRelatedEntity(true);
return configuration;
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false,\"entityCacheExpiration\":300}",
true,
"{\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false,\"entityCacheExpiration\":300}",
true,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}"),
// config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}",
false,
"{\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityNamePattern\":\"$[name]\"," +
"\"entityTypePattern\":\"$[type]\",\"relationType\":\"Contains\"," +
"\"createEntityIfNotExists\":false,\"removeCurrentRelations\":false," +
"\"changeOriginatorToRelatedEntity\":false}")
);
}
}

604
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbDeleteRelationNodeTest.java

@ -0,0 +1,604 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbDeleteRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE,
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,"));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown() {
return supportedEntityTypes.stream().filter(entityType -> !entityType.equals(EntityType.TENANT)).map(Arguments::of);
}
private static final TenantId tenantId = new TenantId(UUID.fromString("6fdb457d-0910-401c-8880-abc251e6a1e2"));
private static final DeviceId deviceId = new DeviceId(UUID.fromString("4eef91a7-8865-4c3c-837d-ed6f6577508b"));
private static final AssetId assetId = new AssetId(UUID.fromString("f4fd3b10-3f36-4d46-a162-5e62050774cc"));
private static final CustomerId customerId = new CustomerId(UUID.fromString("ab890af2-3622-41e0-ac94-14d50af84348"));
private static final EntityViewId entityViewId = new EntityViewId(UUID.fromString("39ce8d03-52a3-4aa8-b561-267d1d9d68b5"));
private static final EdgeId edgeId = new EdgeId(UUID.fromString("dc4f9809-b6f9-48f9-8057-2737216cfdf7"));
private static final DashboardId dashboardId = new DashboardId(UUID.fromString("fda72baa-c882-4723-9693-25995dc37bc5"));
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyConditions() {
return Stream.of(
Arguments.of(new Device(deviceId)),
Arguments.of(new Asset(assetId)),
Arguments.of(new Customer(customerId)),
Arguments.of(new EntityView(entityViewId)),
Arguments.of(new Edge(edgeId)),
Arguments.of(new Dashboard(dashboardId)),
Arguments.of(new Tenant(tenantId))
);
}
private final DeviceId originatorId = new DeviceId(UUID.fromString("574c9840-0885-4d12-be69-f557d7471a78"));
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor();
@Mock
private TbContext ctxMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@Mock
private RelationService relationServiceMock;
private TbDeleteRelationNode node;
private TbDeleteRelationNodeConfiguration config;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbDeleteRelationNode());
config = new TbDeleteRelationNodeConfiguration().defaultConfiguration();
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbDeleteRelationNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getDirection()).isEqualTo(EntitySearchDirection.FROM);
assertThat(defaultConfig.getRelationType()).isEqualTo(EntityRelation.CONTAINS_TYPE);
assertThat(defaultConfig.getEntityNamePattern()).isEqualTo("");
assertThat(defaultConfig.getEntityTypePattern()).isEqualTo(null);
assertThat(defaultConfig.getEntityType()).isEqualTo(null);
assertThat(defaultConfig.isDeleteForSingleEntity()).isFalse();
}
@ParameterizedTest
@EnumSource(EntityType.class)
void givenEntityType_whenInit_thenVerifyExceptionThrownIfTypeIsUnsupported(EntityType entityType) {
// GIVEN
config.setEntityType(entityType);
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN-THEN
if (unsupportedEntityTypes.contains(entityType)) {
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Unsupported entity type '" + entityType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
} else {
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)).doesNotThrowAnyException();
}
verifyNoInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyEntityNotFoundExceptionThrown(EntityType entityType) throws TbNodeException {
// GIVEN
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var mockMethodCallsMap = mockEntityServiceCallsEntityNotFound();
mockMethodCallsMap.get(entityType).run();
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// todo fix TestDbCallbackExecutor exception handling.
switch (entityType) {
case CUSTOMER -> {
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage(EntityType.CUSTOMER.getNormalName() + " with title 'EntityName' doesn't exist!");
}
case DEVICE, ASSET -> {
node.onMsg(ctxMock, msg);
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue())
.isInstanceOf(NoSuchElementException.class)
.hasMessage(entityType.getNormalName() + " with name 'EntityName' doesn't exist!");
}
default -> assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(RuntimeException.class).hasCauseInstanceOf(NoSuchElementException.class);
}
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationDeletedAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationFailedToDeleteAndOutMsgFailure(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!");
}
@ParameterizedTest
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions")
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationNotFoundAndOutMsgSuccess(HasId entity) throws TbNodeException {
// GIVEN
var entityId = (EntityId) entity.getId();
var entityType = entityId.getEntityType();
config.setEntityType(entityType);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
config.setDeleteForSingleEntity(true);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
var mockMethodCallsMap = mockEntityServiceCalls();
mockMethodCallsMap.get(entityType).accept(entity);
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var verifyMethodCallsMap = verifyEntityServiceCalls();
verifyMethodCallsMap.get(entityType).accept(entity);
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
verify(ctxMock).getDbCallbackExecutor();
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationsDeletedAndOutMsgSuccess() throws TbNodeException {
// GIVEN
config.setEntityType(EntityType.DEVICE);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
verify(ctxMock).tellSuccess(eq(msg));
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
verifyNoMoreInteractions(ctxMock, relationServiceMock);
}
@Test
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationFailedToDeleteAndOutMsgFailure() throws TbNodeException {
// GIVEN
config.setEntityType(EntityType.DEVICE);
config.setEntityNamePattern("${name}");
config.setEntityTypePattern("${type}");
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
when(ctxMock.getTenantId()).thenReturn(tenantId);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
var relationToDelete = new EntityRelation();
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete)));
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(false));
var md = getMetadataWithNameTemplate();
var msg = getTbMsg(originatorId, md);
// WHEN
node.onMsg(ctxMock, msg);
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON));
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete));
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellSuccess(any());
verifyNoMoreInteractions(ctxMock, relationServiceMock);
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!");
}
private Map<EntityType, Runnable> mockEntityServiceCallsEntityNotFound() {
return Map.of(
EntityType.DEVICE, () -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.ASSET, () -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.CUSTOMER, () -> {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.empty());
},
EntityType.ENTITY_VIEW, () -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.EDGE, () -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(null);
},
EntityType.USER, () -> {
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(null);
},
EntityType.DASHBOARD, () -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(null);
}
);
}
private Map<EntityType, Consumer<HasId>> mockEntityServiceCalls() {
return Map.of(
EntityType.DEVICE, hasId -> {
var device = (Device) hasId;
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(device);
},
EntityType.ASSET, hasId -> {
var asset = (Asset) hasId;
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(asset);
},
EntityType.CUSTOMER, hasId -> {
var customer = (Customer) hasId;
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.ofNullable(customer));
},
EntityType.ENTITY_VIEW, hasId -> {
var entityView = (EntityView) hasId;
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(entityView);
},
EntityType.EDGE, hasId -> {
var edge = (Edge) hasId;
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(edge);
},
EntityType.USER, hasId -> {
var user = (User) hasId;
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(user);
},
EntityType.DASHBOARD, hasId -> {
var dashboard = (Dashboard) hasId;
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(dashboard);
},
EntityType.TENANT, hasId -> {
// do nothing. tenantId returned by ctxMock.
}
);
}
private Map<EntityType, Consumer<HasId>> verifyEntityServiceCalls() {
return Map.of(
EntityType.DEVICE, hasId -> {
verify(deviceServiceMock).findDeviceByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(deviceServiceMock);
},
EntityType.ASSET, hasId -> {
verify(assetServiceMock).findAssetByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(assetServiceMock);
},
EntityType.CUSTOMER, hasId -> {
verify(customerServiceMock).findCustomerByTenantIdAndTitle(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(customerServiceMock);
},
EntityType.ENTITY_VIEW, hasId -> {
verify(entityViewServiceMock).findEntityViewByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(entityViewServiceMock);
},
EntityType.EDGE, hasId -> {
verify(edgeServiceMock).findEdgeByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(edgeServiceMock);
},
EntityType.USER, hasId -> {
verify(userServiceMock).findUserByTenantIdAndEmail(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(userServiceMock);
},
EntityType.DASHBOARD, hasId -> {
verify(dashboardServiceMock).findFirstDashboardInfoByTenantIdAndName(eq(tenantId), eq("EntityName"));
verifyNoMoreInteractions(dashboardServiceMock);
},
EntityType.TENANT, hasId -> {
}
);
}
private TbMsg getTbMsg(EntityId originator, TbMsgMetaData metaData) {
return TbMsg.newMsg(TbMsgType.NA, originator, metaData, TbMsg.EMPTY_JSON_OBJECT);
}
private TbMsgMetaData getMetadataWithNameTemplate() {
var metaData = new TbMsgMetaData();
metaData.putValue("name", "EntityName");
return metaData;
}
@Override
protected TbNode getTestNode() {
return node;
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}",
true,
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}",
true,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"),
// config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}",
false,
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," +
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}")
);
}
}

313
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNodeTest.java

@ -0,0 +1,313 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
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.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TbUnassignFromCustomerNodeTest extends AbstractRuleNodeUpgradeTest {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET,
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD);
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", "));
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values())
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet());
private final Device DEVICE = new Device();
private final Asset ASSET = new Asset();
private final EntityView ENTITY_VIEW = new EntityView();
private final Edge EDGE = new Edge();
private final Dashboard DASHBOARD = new Dashboard();
private final TenantId TENANT_ID = new TenantId(UUID.fromString("06fcc15f-2677-436d-a1cb-7754bd0bcccf"));
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() {
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type)));
}
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg() {
return supportedEntityTypes.stream()
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5))));
}
private TbUnassignFromCustomerNode node;
private TbUnassignFromCustomerNodeConfiguration config;
@Mock
private TbContext ctxMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private EdgeService edgeServiceMock;
@Mock
private DashboardService dashboardServiceMock;
@BeforeEach
public void setUp() throws TbNodeException {
node = spy(new TbUnassignFromCustomerNode());
config = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration();
}
@Override
protected TbNode getTestNode() {
return node;
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
var defaultConfig = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty();
}
@ParameterizedTest
@MethodSource
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) {
// GIVEN
var originator = toOriginator(originatorType);
var msg = getTbMsg(originator);
// WHEN
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType +
"'! Only " + supportedEntityTypesStr + " types are allowed.");
verifyNoInteractions(ctxMock);
verifyNoInteractions(customerServiceMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
// we search for the customer only if incoming message originator is dashboard.
if (type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
var customer = createCustomer(customerTitle);
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.of(customer));
}
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
@ParameterizedTest
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg")
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerSearchedAndNotFoundOnlyForDashboardOriginator(EntityType type, String customerTitle) throws TbNodeException {
// GIVEN
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
config.setCustomerNamePattern(customerTitle);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
var originator = toOriginator(type);
var msg = getTbMsg(originator);
// we search for the customer only if incoming message originator is dashboard.
if (type.equals(EntityType.DASHBOARD)) {
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty());
// DASHBOARD WHEN
node.onMsg(ctxMock, msg);
// DASHBOARD THEN
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture());
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!");
verifyNoMoreInteractions(customerServiceMock);
verifyNoMoreInteractions(ctxMock);
return;
}
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes();
entityTypeToEntityIdConsumerMap.get(type).accept(originator);
// OTHER TYPES WHEN
node.onMsg(ctxMock, msg);
// OTHER TYPES THEN
verifyMsgSuccess(msg);
verifyNoMoreInteractions(ctxMock);
}
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() {
return Map.of(
EntityType.DEVICE, id -> {
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.unassignDeviceFromCustomer(eq(TENANT_ID), (DeviceId) eq(id)))
.thenReturn(DEVICE);
},
EntityType.ASSET, id -> {
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.unassignAssetFromCustomer(eq(TENANT_ID), (AssetId) eq(id)))
.thenReturn(ASSET);
},
EntityType.ENTITY_VIEW, id -> {
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock);
when(entityViewServiceMock.unassignEntityViewFromCustomer(eq(TENANT_ID), (EntityViewId) eq(id)))
.thenReturn(ENTITY_VIEW);
},
EntityType.EDGE, id -> {
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.unassignEdgeFromCustomer(eq(TENANT_ID), (EdgeId) eq(id)))
.thenReturn(EDGE);
},
EntityType.DASHBOARD, id -> {
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock);
when(dashboardServiceMock.unassignDashboardFromCustomer(eq(TENANT_ID), (DashboardId) eq(id), any()))
.thenReturn(DASHBOARD);
}
);
}
private void verifyMsgSuccess(TbMsg expectedMsg) {
verify(ctxMock).tellSuccess(eq(expectedMsg));
verify(ctxMock, never()).tellFailure(any(), any());
}
private Customer createCustomer(String customerTitle) {
var customer = new Customer();
customer.setTitle(customerTitle);
customer.setId(new CustomerId(UUID.randomUUID()));
customer.setTenantId(TENANT_ID);
return customer;
}
private TbMsg getTbMsg(EntityId originator) {
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
}
private static EntityId toOriginator(EntityType type) {
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString());
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// default config for version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\",\"customerCacheExpiration\":300}",
true,
"{\"customerNamePattern\":\"\"}"),
// default config for version 1 with upgrade from version 0
Arguments.of(0,
"{\"customerNamePattern\":\"\"}",
false,
"{\"customerNamePattern\":\"\"}")
);
}
}

37
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java

@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.ArgumentCaptor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest;
import org.thingsboard.rule.engine.TestDbCallbackExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.AssetId;
@ -42,6 +45,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -52,16 +56,17 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TbCheckRelationNodeTest {
class TbCheckRelationNodeTest extends AbstractRuleNodeUpgradeTest {
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID());
private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
private final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID());
private final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor();
private final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT);
private TbCheckRelationNode node;
@ -77,7 +82,7 @@ class TbCheckRelationNodeTest {
when(ctx.getRelationService()).thenReturn(relationService);
when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
node = new TbCheckRelationNode();
node = spy(new TbCheckRelationNode());
}
@AfterEach
@ -310,4 +315,24 @@ class TbCheckRelationNodeTest {
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
// Rule nodes upgrade
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() {
return Stream.of(
// version 0 config, FROM direction.
Arguments.of(0,
"{\"checkForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}",
true,
"{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}"),
// version 0 config, TO direction.
Arguments.of(0,
"{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}",
true,
"{\"checkForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\",\"entityId\":\"1943b1eb-2811-4373-846d-6ca2f527bf9e\",\"relationType\":\"Contains\"}")
);
}
@Override
protected TbNode getTestNode() {
return node;
}
}

Loading…
Cancel
Save