Browse Source

Refactor relations validation

pull/13806/head
Dmytro Skarzhynets 1 year ago
parent
commit
21c17e308d
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 88
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  3. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
  4. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  5. 257
      application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java
  6. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  7. 55
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
  8. 139
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

4
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -509,8 +509,8 @@ public abstract class BaseController {
}
}
void checkParameter(String name, String param) throws ThingsboardException {
if (StringUtils.isEmpty(param)) {
static void checkParameter(String name, String param) throws ThingsboardException {
if (StringUtils.isBlank(param)) {
throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}

88
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -17,15 +17,15 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.entity.relation.TbEntityRelationService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -76,10 +77,9 @@ public class EntityRelationController extends BaseController {
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@PostMapping("/relation")
public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
@RequestBody EntityRelation relation) throws ThingsboardException {
doSave(relation);
}
@ -88,29 +88,26 @@ public class EntityRelationController extends BaseController {
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@PostMapping("/v2/relation")
public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
return doSave(relation);
}
private EntityRelation doSave(EntityRelation relation) throws ThingsboardException {
checkNotNull(relation);
checkCanCreateRelation(relation.getFrom());
checkCanCreateRelation(relation.getTo());
if (relation.getTypeGroup() == null) {
relation.setTypeGroup(RelationTypeGroup.COMMON);
}
ConstraintValidator.validateFields(relation);
checkCanCreateRelation(relation.getFrom());
checkCanCreateRelation(relation.getTo());
return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete Relation (deleteRelation)",
notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
@ResponseStatus(value = HttpStatus.OK)
@DeleteMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
public void deleteRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@ -123,21 +120,19 @@ public class EntityRelationController extends BaseController {
@ApiOperation(value = "Delete Relation (deleteRelationV2)",
notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
@ResponseStatus(value = HttpStatus.OK)
@DeleteMapping(value = "/v2/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
}
private EntityRelation doDelete(String strFromId, String strFromType, String strRelationType, String strRelationTypeGroup, String strToId, String strToType) throws ThingsboardException {
checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType);
checkParameter(RELATION_TYPE, strRelationType);
checkParameter(TO_ID, strToId);
checkParameter(TO_TYPE, strToType);
EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
@ -154,8 +149,7 @@ public class EntityRelationController extends BaseController {
notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"})
@ResponseStatus(value = HttpStatus.OK)
@DeleteMapping(value = "/relations", params = {"entityId", "entityType"})
public void deleteRelations(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException {
checkParameter("entityId", strId);
@ -168,8 +162,7 @@ public class EntityRelationController extends BaseController {
@ApiOperation(value = "Get Relation (getRelation)",
notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relation", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
@ResponseBody
@GetMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
public EntityRelation getRelation(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@ -178,7 +171,6 @@ public class EntityRelationController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType);
checkParameter(RELATION_TYPE, strRelationType);
checkParameter(TO_ID, strToId);
checkParameter(TO_TYPE, strToType);
EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
@ -193,8 +185,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE})
@ResponseBody
@GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE})
public List<EntityRelation> findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@ -211,8 +202,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE})
@ResponseBody
@GetMapping(value = "/relations/info", params = {FROM_ID, FROM_TYPE})
public List<EntityRelationInfo> findInfoByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@ -229,8 +219,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE})
@ResponseBody
@GetMapping(value = "/relations", params = {FROM_ID, FROM_TYPE, RELATION_TYPE})
public List<EntityRelation> findByFrom(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@ -249,8 +238,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation objects for the specified entity by the 'to' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE})
@ResponseBody
@GetMapping(value = "/relations", params = {TO_ID, TO_TYPE})
public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@ -267,8 +255,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {TO_ID, TO_TYPE})
@ResponseBody
@GetMapping(value = "/relations/info", params = {TO_ID, TO_TYPE})
public List<EntityRelationInfo> findInfoByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@ -285,8 +272,7 @@ public class EntityRelationController extends BaseController {
notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE, RELATION_TYPE})
@ResponseBody
@GetMapping(value = "/relations", params = {TO_ID, TO_TYPE, RELATION_TYPE})
public List<EntityRelation> findByTo(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
@ -306,8 +292,7 @@ public class EntityRelationController extends BaseController {
"The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " +
"See 'Model' tab of the Parameters for more info.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/relations")
public List<EntityRelation> findByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
@ -322,8 +307,7 @@ public class EntityRelationController extends BaseController {
"The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " +
"See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/relations/info", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/relations/info")
public List<EntityRelationInfo> findInfoByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
@ -357,15 +341,15 @@ public class EntityRelationController extends BaseController {
}).collect(Collectors.toList());
}
private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
RelationTypeGroup result = defaultValue;
if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length() > 0) {
try {
result = RelationTypeGroup.valueOf(strRelationTypeGroup);
} catch (IllegalArgumentException e) {
}
private static RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
if (StringUtils.isBlank(strRelationTypeGroup)) {
return defaultValue;
}
try {
return RelationTypeGroup.valueOf(strRelationTypeGroup);
} catch (IllegalArgumentException e) {
return defaultValue;
}
return result;
}
}

5
application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.entitiy.entity.relation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -33,7 +32,6 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultTbEntityRelationService extends AbstractTbEntityService implements TbEntityRelationService {
private final RelationService relationService;
@ -71,7 +69,7 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
}
@Override
public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException {
public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) {
try {
relationService.deleteEntityCommonRelations(tenantId, entityId);
logEntityActionService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user);
@ -81,4 +79,5 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
throw e;
}
}
}

5
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -197,8 +197,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType);
ArgumentMatcher<HasName> matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass());
ArgumentMatcher<EntityId> matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass());
ArgumentMatcher<CustomerId> matcherCustomerId = customerId == null ?
argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId);
ArgumentMatcher<CustomerId> matcherCustomerId = customerId == null ? argument -> true : actualCustomerId -> actualCustomerId.equals(customerId);
ArgumentMatcher<UserId> matcherUserId = userId == null ?
argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId);
testLogEntityActionAdditionalInfoAny(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime,
@ -623,7 +622,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
return fieldName + " length must be equal or less than 255";
}
protected String msgErrorNoFound(String entityClassName, String entityIdStr) {
protected static String msgErrorNoFound(String entityClassName, String entityIdStr) {
return entityClassName + " with id [" + entityIdStr + "] is not found";
}

257
application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java

@ -17,19 +17,14 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil;
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.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -39,8 +34,6 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Collections;
@ -48,57 +41,29 @@ import java.util.List;
import java.util.UUID;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@DaoSqlTest
public class EntityRelationControllerTest extends AbstractControllerTest {
public static final String BASE_DEVICE_NAME = "Test dummy device";
@Autowired
RelationService relationService;
private IdComparator<EntityView> idComparator;
private Tenant savedTenant;
private User tenantAdmin;
private Device mainDevice;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
idComparator = new IdComparator<>();
Tenant tenant = new Tenant();
tenant.setTitle("Test tenant");
savedTenant = saveTenant(tenant);
Assert.assertNotNull(savedTenant);
loginTenantAdmin();
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
Device device = new Device();
var device = new Device();
device.setName("Main test device");
device.setType("default");
mainDevice = doPost("/api/device", device, Device.class);
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
deleteTenant(savedTenant.getId());
}
@Test
public void testSaveAndFindRelation() throws Exception {
Device device = buildSimpleDevice("Test device 1");
Device device = createDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
Mockito.reset(tbClusterService, auditLogService);
@ -116,57 +81,117 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
Assert.assertEquals("Found relation is not equals origin!", relation, foundRelation);
testNotifyEntityAllOneTimeRelation(foundRelation,
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(),
ActionType.RELATION_ADD_OR_UPDATE, foundRelation);
}
@Test
public void testSaveWithDeviceFromNotCreated() throws Exception {
Device device = new Device();
device.setName("Test device 2");
device.setType("default");
EntityRelation relation = createFromRelation(device, mainDevice, "CONTAINS");
Mockito.reset(tbClusterService, auditLogService);
public void testSaveRelationFromValidation() throws Exception {
// GIVEN
var relation = new EntityRelation();
relation.setFrom(null);
relation.setTo(mainDevice.getId());
relation.setType("Contains");
// WHEN-THEN
for (String endpoint : List.of("/api/relation", "/api/v2/relation")) {
doPost(endpoint, relation)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: from must not be null")));
}
}
doPost("/api/relation", relation)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Parameter entityId can't be empty!")));
@Test
public void testSaveRelationToValidation() throws Exception {
// GIVEN
var relation = new EntityRelation();
relation.setFrom(mainDevice.getId());
relation.setTo(null);
relation.setType("Contains");
// WHEN-THEN
for (String endpoint : List.of("/api/relation", "/api/v2/relation")) {
doPost(endpoint, relation)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: to must not be null")));
}
}
testNotifyEntityNever(mainDevice.getId(), null);
@Test
public void testSaveRelationRelationTypeValidation() throws Exception {
// GIVEN
var device = createDevice("Test device");
EntityRelation relationTypeNull = createFromRelation(mainDevice, device, null);
EntityRelation relationTypeEmpty = createFromRelation(mainDevice, device, "");
EntityRelation relationTypeBlank = createFromRelation(mainDevice, device, " ");
EntityRelation relationTypeContainsNullChar = createFromRelation(mainDevice, device, "null char \u0000");
EntityRelation relationTypeTooLong = createFromRelation(mainDevice, device, "a".repeat(256));
// WHEN-THEN
for (String endpoint : List.of("/api/relation", "/api/v2/relation")) {
doPost(endpoint, relationTypeNull)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: type must not be blank")));
doPost(endpoint, relationTypeEmpty)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: type must not be blank")));
doPost(endpoint, relationTypeBlank)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: type must not be blank")));
doPost(endpoint, relationTypeContainsNullChar)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: type should not contain 0x00 symbol")));
doPost(endpoint, relationTypeTooLong)
.andExpect(status().isBadRequest())
.andExpect(statusReason(is("Validation error: type length must be equal or less than 255")));
}
}
@Test
public void testSaveWithDeviceToNotCreated() throws Exception {
Device device = new Device();
device.setName("Test device 2");
device.setType("default");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
public void testSaveRelationFromNonexistentEntity() throws Exception {
// GIVEN
var nonexistentDevice = new Device();
nonexistentDevice.setId(new DeviceId(UUID.randomUUID()));
nonexistentDevice.setName("Nonexistent device");
nonexistentDevice.setType("default");
EntityRelation relation = createFromRelation(nonexistentDevice, mainDevice, "CONTAINS");
Mockito.reset(tbClusterService, auditLogService);
doPost("/api/relation", relation)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Parameter entityId can't be empty!")));
// WHEN-THEN
for (String endpoint : List.of("/api/relation", "/api/v2/relation")) {
doPost(endpoint, relation)
.andExpect(status().isNotFound())
.andExpect(statusReason(is(msgErrorNoFound("Device", nonexistentDevice.getId().toString()))));
testNotifyEntityNever(mainDevice.getId(), null);
testNotifyEntityNever(mainDevice.getId(), null);
}
}
@Test
public void testSaveWithDeviceToMissing() throws Exception {
Device device = new Device();
device.setName("Test device 2");
device.setType("default");
device.setId(new DeviceId(UUID.randomUUID()));
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
public void testSaveRelationToNonexistentEntity() throws Exception {
// GIVEN
var nonexistentDevice = new Device();
nonexistentDevice.setId(new DeviceId(UUID.randomUUID()));
nonexistentDevice.setName("Nonexistent device");
nonexistentDevice.setType("default");
EntityRelation relation = createFromRelation(mainDevice, nonexistentDevice, "CONTAINS");
Mockito.reset(tbClusterService, auditLogService);
doPost("/api/relation", relation)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", device.getId().getId().toString()))));
// WHEN-THEN
for (String endpoint : List.of("/api/relation", "/api/v2/relation")) {
doPost(endpoint, relation)
.andExpect(status().isNotFound())
.andExpect(statusReason(is(msgErrorNoFound("Device", nonexistentDevice.getId().toString()))));
testNotifyEntityNever(mainDevice.getId(), null);
testNotifyEntityNever(mainDevice.getId(), null);
}
}
@Test
@ -178,7 +203,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME);
EntityRelation relationTest = createFromRelation(mainDevice, mainDevice, "TEST_NOTIFY_ENTITY");
testNotifyEntityAllManyRelation(relationTest, savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
testNotifyEntityAllManyRelation(relationTest, tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(),
ActionType.RELATION_ADD_OR_UPDATE, numOfDevices);
String url = String.format("/api/relations?fromId=%s&fromType=%s",
@ -204,7 +229,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
final int numOfDevices = 30;
createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME);
Device device = buildSimpleDevice("Unique dummy test device ");
Device device = createDevice("Unique dummy test device ");
String relationType = "TEST";
EntityRelation relation = createFromRelation(mainDevice, device, relationType);
@ -221,7 +246,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
final int numOfDevices = 30;
createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME);
Device device = buildSimpleDevice("Unique dummy test device ");
Device device = createDevice("Unique dummy test device ");
String relationType = "TEST";
EntityRelation relation = createFromRelation(mainDevice, device, relationType);
@ -240,7 +265,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
final int numOfDevices = 30;
createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME);
Device device = buildSimpleDevice("Unique dummy test device ");
Device device = createDevice("Unique dummy test device ");
String relationType = "TEST";
EntityRelation relation = createFromRelation(device, mainDevice, relationType);
@ -252,13 +277,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
assertFoundList(url, 1);
}
@Test
public void testSaveAndFindRelationsByToWithRelationTypeOther() throws Exception {
final int numOfDevices = 30;
createDevicesByFrom(numOfDevices, BASE_DEVICE_NAME);
Device device = buildSimpleDevice("Unique dummy test device ");
Device device = createDevice("Unique dummy test device ");
String relationType = "TEST";
EntityRelation relation = createFromRelation(device, mainDevice, relationType);
@ -280,9 +304,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
mainDevice.getUuidId(), EntityType.DEVICE
);
List<EntityRelationInfo> relationsInfos =
JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {
});
List<EntityRelationInfo> relationsInfos = JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {});
Assert.assertNotNull("Relations is not found!", relationsInfos);
Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!",
@ -299,57 +321,49 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
mainDevice.getUuidId(), EntityType.DEVICE
);
List<EntityRelationInfo> relationsInfos =
JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {
});
List<EntityRelationInfo> relationsInfos = JacksonUtil.convertValue(doGet(url, JsonNode.class), new TypeReference<>() {});
Assert.assertNotNull("Relations is not found!", relationsInfos);
Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!",
numOfDevices, relationsInfos.size());
Assert.assertEquals("List of found relationsInfos is not equal to number of created relations!", numOfDevices, relationsInfos.size());
assertRelationsInfosByTo(relationsInfos);
}
@Test
public void testDeleteRelation() throws Exception {
Device device = buildSimpleDevice("Test device 1");
// GIVEN
Device device = createDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE,
"CONTAINS", device.getUuidId(), EntityType.DEVICE
);
EntityRelation foundRelation = doGet(url, EntityRelation.class);
Assert.assertNotNull("Relation is not found!", foundRelation);
Assert.assertEquals("Found relation is not equals origin!", relation, foundRelation);
Mockito.reset(tbClusterService, auditLogService);
// WHEN
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE,
"CONTAINS", device.getUuidId(), EntityType.DEVICE
);
var deletedRelation = doDelete(deleteUrl, EntityRelation.class);
// THEN
testNotifyEntityAllOneTimeRelation(deletedRelation,
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(),
ActionType.RELATION_DELETED, deletedRelation);
doGet(url).andExpect(status().is4xxClientError());
getRelation(relation)
.andExpect(status().isNotFound())
.andExpect(statusReason(is(msgErrorNotFound)));
}
@Test
public void testDeleteRelationWithOtherFromDeviceError() throws Exception {
Device device = buildSimpleDevice("Test device 1");
Device device = createDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
Device device2 = buildSimpleDevice("Test device 2");
Device device2 = createDevice("Test device 2");
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
device2.getUuidId(), EntityType.DEVICE,
"CONTAINS", device.getUuidId(), EntityType.DEVICE
@ -366,12 +380,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test
public void testDeleteRelationWithOtherToDeviceError() throws Exception {
Device device = buildSimpleDevice("Test device 1");
Device device = createDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
Device device2 = buildSimpleDevice("Test device 2");
Device device2 = createDevice("Test device 2");
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE,
"CONTAINS", device2.getUuidId(), EntityType.DEVICE
@ -411,7 +425,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
doDelete(url).andExpect(status().isOk());
testNotifyEntityOneTimeMsgToEdgeServiceNever(null, mainDevice.getId(), mainDevice.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(),
ActionType.RELATIONS_DELETED);
Assert.assertTrue(
@ -442,8 +456,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
List<EntityRelation> relations = readResponse(
doPost("/api/relations", query).andExpect(status().isOk()),
new TypeReference<List<EntityRelation>>() {
}
new TypeReference<>() {}
);
assertFoundRelations(relations, numOfDevices);
@ -467,8 +480,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
List<EntityRelation> relations = readResponse(
doPost("/api/relations", query).andExpect(status().isOk()),
new TypeReference<>() {
}
new TypeReference<>() {}
);
assertFoundRelations(relations, numOfDevices);
@ -526,11 +538,11 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test
public void testCreateRelationFromTenantToDevice() throws Exception {
EntityRelation relation = new EntityRelation(tenantAdmin.getTenantId(), mainDevice.getId(), "CONTAINS");
EntityRelation relation = new EntityRelation(tenantId, mainDevice.getId(), "CONTAINS");
relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
tenantAdmin.getTenantId(), EntityType.TENANT,
tenantId, EntityType.TENANT,
"CONTAINS", mainDevice.getUuidId(), EntityType.DEVICE
);
@ -542,12 +554,12 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test
public void testCreateRelationFromDeviceToTenant() throws Exception {
EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantAdmin.getTenantId(), "CONTAINS");
EntityRelation relation = new EntityRelation(mainDevice.getId(), tenantId, "CONTAINS");
relation = doPost("/api/v2/relation", relation, EntityRelation.class);
String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
mainDevice.getUuidId(), EntityType.DEVICE,
"CONTAINS", tenantAdmin.getTenantId(), EntityType.TENANT
"CONTAINS", tenantId, EntityType.TENANT
);
EntityRelation foundRelation = doGet(url, EntityRelation.class);
@ -558,7 +570,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
@Test
public void testSaveAndFindRelationDifferentTenant() throws Exception {
Device device = buildSimpleDevice("Test device 1");
Device device = createDevice("Test device 1");
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
@ -577,12 +589,20 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
deleteDifferentTenant();
}
private Device buildSimpleDevice(String name) throws Exception {
Device device = new Device();
private Device createDevice(String name) {
var device = new Device();
device.setName(name);
device.setType("default");
device = doPost("/api/device", device, Device.class);
return device;
return doPost("/api/device", device, Device.class);
}
private ResultActions getRelation(EntityRelation relation) throws Exception {
return doGet("/api/relation?" +
"fromId=" + relation.getFrom().getId() +
"&fromType=" + relation.getFrom().getEntityType() +
"&relationType=" + relation.getType() +
"&toId=" + relation.getTo().getId() +
"&toType=" + relation.getTo().getEntityType());
}
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) {
@ -591,7 +611,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
private void createDevicesByFrom(int numOfDevices, String baseName) throws Exception {
for (int i = 0; i < numOfDevices; i++) {
Device device = buildSimpleDevice(baseName + i);
Device device = createDevice(baseName + i);
EntityRelation relation = createFromRelation(mainDevice, device, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
@ -600,7 +620,7 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
private void createDevicesByTo(int numOfDevices, String baseName) throws Exception {
for (int i = 0; i < numOfDevices; i++) {
Device device = buildSimpleDevice(baseName + i);
Device device = createDevice(baseName + i);
EntityRelation relation = createFromRelation(device, mainDevice, "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
}
@ -633,4 +653,5 @@ public class EntityRelationControllerTest extends AbstractControllerTest {
Assert.assertEquals("Wrong relationType!", "CONTAINS", info.getType());
}
}
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -26,9 +26,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.List;
/**
* Created by ashvayka on 27.04.17.
*/
public interface RelationService {
ListenableFuture<Boolean> checkRelationAsync(TenantId tenantId, EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);

55
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java

@ -18,11 +18,11 @@ package org.thingsboard.server.common.data.relation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ObjectType;
@ -30,16 +30,19 @@ import org.thingsboard.server.common.data.edqs.EdqsObject;
import org.thingsboard.server.common.data.edqs.EdqsObjectKey;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar;
import java.io.Serial;
import java.io.Serializable;
import java.util.UUID;
@Slf4j
@Data
@Schema
@EqualsAndHashCode(exclude = "additionalInfoBytes")
@ToString(exclude = {"additionalInfoBytes"})
public class EntityRelation implements HasVersion, Serializable, EdqsObject {
@Serial
private static final long serialVersionUID = 2807343040519543363L;
public static final String EDGE_TYPE = "ManagedByEdge";
@ -47,18 +50,26 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject {
public static final String MANAGES_TYPE = "Manages";
public static final String USES_TYPE = "Uses";
@Setter
@NotNull
@Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_WRITE)
private EntityId from;
@Setter
@NotNull
@Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_WRITE)
private EntityId to;
@Setter
@Length(fieldName = "type")
@NotBlank
@NoNullChar
@Length(max = 255, fieldName = "type")
@Schema(description = "String value of relation type.", example = "Contains")
private String type;
@Setter
@NotNull
@Schema(description = "Represents the type group of the relation.", example = "COMMON")
private RelationTypeGroup typeGroup;
@Getter
@Setter
private Long version;
private transient JsonNode additionalInfo;
@JsonIgnore
private byte[] additionalInfoBytes;
@ -92,27 +103,7 @@ public class EntityRelation implements HasVersion, Serializable, EdqsObject {
this.version = entityRelation.getVersion();
}
@Schema(description = "JSON object with [from] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY)
public EntityId getFrom() {
return from;
}
@Schema(description = "JSON object with [to] Entity Id.", accessMode = Schema.AccessMode.READ_ONLY)
public EntityId getTo() {
return to;
}
@Schema(description = "String value of relation type.", example = "Contains")
public String getType() {
return type;
}
@Schema(description = "Represents the type group of the relation.", example = "COMMON")
public RelationTypeGroup getTypeGroup() {
return typeGroup;
}
@Schema(description = "Additional parameters of the relation", implementation = com.fasterxml.jackson.databind.JsonNode.class)
@Schema(description = "Additional parameters of the relation", implementation = JsonNode.class)
public JsonNode getAdditionalInfo() {
return BaseDataWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes);
}

139
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -19,7 +19,6 @@ import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@ -66,14 +65,12 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.dao.service.Validator.validateId;
/**
* Created by ashvayka on 28.04.17.
*/
@Service
@Slf4j
public class BaseRelationService implements RelationService {
@Service
class BaseRelationService implements RelationService {
private final RelationDao relationDao;
private final EntityService entityService;
@ -81,7 +78,7 @@ public class BaseRelationService implements RelationService {
private final ApplicationEventPublisher eventPublisher;
private final JpaExecutorService executor;
private final JpaRelationQueryExecutorService relationsExecutor;
protected ScheduledExecutorService timeoutExecutorService;
private ScheduledExecutorService timeoutExecutorService;
@Value("${sql.relations.query_timeout:20}")
private Integer relationQueryTimeout;
@ -179,7 +176,11 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation) {
log.trace("Executing saveRelationAsync [{}]", relation);
validate(relation);
try {
validate(relation);
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
}
var future = relationDao.saveRelationAsync(tenantId, relation);
return Futures.transform(future, savedRelation -> {
if (savedRelation != null) {
@ -187,7 +188,7 @@ public class BaseRelationService implements RelationService {
eventPublisher.publishEvent(new RelationActionEvent(tenantId, savedRelation, ActionType.RELATION_ADD_OR_UPDATE));
}
return savedRelation != null;
}, MoreExecutors.directExecutor());
}, directExecutor());
}
@Override
@ -205,7 +206,11 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<Boolean> deleteRelationAsync(TenantId tenantId, EntityRelation relation) {
log.trace("Executing deleteRelationAsync [{}]", relation);
validate(relation);
try {
validate(relation);
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
}
var future = relationDao.deleteRelationAsync(tenantId, relation);
return Futures.transform(future, deletedRelation -> {
if (deletedRelation != null) {
@ -213,7 +218,7 @@ public class BaseRelationService implements RelationService {
eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedRelation, ActionType.RELATION_DELETED));
}
return deletedRelation != null;
}, MoreExecutors.directExecutor());
}, directExecutor());
}
@Override
@ -239,7 +244,7 @@ public class BaseRelationService implements RelationService {
eventPublisher.publishEvent(new RelationActionEvent(tenantId, deletedEvent, ActionType.RELATION_DELETED));
}
return deletedEvent != null;
}, MoreExecutors.directExecutor());
}, directExecutor());
}
@Transactional
@ -316,17 +321,10 @@ public class BaseRelationService implements RelationService {
log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup);
validate(from);
validateTypeGroup(typeGroup);
ListenableFuture<List<EntityRelation>> relations = executor.submit(() -> relationDao.findAllByFrom(tenantId, from, typeGroup));
return Futures.transformAsync(relations,
relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(tenantId, relation,
EntityRelation::getTo,
EntityRelationInfo::setToName))
);
return Futures.successfulAsList(futures);
}, MoreExecutors.directExecutor());
return Futures.transform(executor.submit(() -> relationDao.findAllByFrom(tenantId, from, typeGroup)),
relations -> relations.stream()
.map(relation -> fetchRelationInfo(tenantId, relation, EntityRelation::getTo, EntityRelationInfo::setToName))
.toList(), directExecutor());
}
@Override
@ -372,26 +370,10 @@ public class BaseRelationService implements RelationService {
log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup);
validate(to);
validateTypeGroup(typeGroup);
ListenableFuture<List<EntityRelation>> relations = findByToAsync(tenantId, to, typeGroup);
return Futures.transformAsync(relations,
relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(tenantId, relation,
EntityRelation::getFrom,
EntityRelationInfo::setFromName))
);
return Futures.successfulAsList(futures);
}, MoreExecutors.directExecutor());
}
private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(TenantId tenantId, EntityRelation relation,
Function<EntityRelation, EntityId> entityIdGetter,
BiConsumer<EntityRelationInfo, String> entityNameSetter) {
EntityRelationInfo relationInfo = new EntityRelationInfo(relation);
entityNameSetter.accept(relationInfo,
entityService.fetchEntityName(tenantId, entityIdGetter.apply(relation)).orElse("N/A"));
return Futures.immediateFuture(relationInfo);
return Futures.transform(findByToAsync(tenantId, to, typeGroup),
relations -> relations.stream()
.map(relation -> fetchRelationInfo(tenantId, relation, EntityRelation::getFrom, EntityRelationInfo::setFromName))
.toList(), directExecutor());
}
@Override
@ -443,7 +425,7 @@ public class BaseRelationService implements RelationService {
}
}
return relations;
}, MoreExecutors.directExecutor());
}, directExecutor());
} catch (Exception e) {
log.warn("Failed to query relations: [{}]", query, e);
throw new RuntimeException(e);
@ -453,24 +435,31 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<List<EntityRelationInfo>> findInfoByQuery(TenantId tenantId, EntityRelationsQuery query) {
log.trace("Executing findInfoByQuery [{}]", query);
ListenableFuture<List<EntityRelation>> relations = findByQuery(tenantId, query);
EntitySearchDirection direction = query.getParameters().getDirection();
return Futures.transformAsync(relations,
relations1 -> {
List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
relations1.forEach(relation ->
futures.add(fetchRelationInfoAsync(tenantId, relation,
relation2 -> direction == EntitySearchDirection.FROM ? relation2.getTo() : relation2.getFrom(),
(EntityRelationInfo relationInfo, String entityName) -> {
if (direction == EntitySearchDirection.FROM) {
relationInfo.setToName(entityName);
} else {
relationInfo.setFromName(entityName);
}
}))
);
return Futures.successfulAsList(futures);
}, MoreExecutors.directExecutor());
Function<EntityRelation, EntityId> entityIdGetter = relation -> direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
BiConsumer<EntityRelationInfo, String> entityNameSetter = (EntityRelationInfo relationInfo, String entityName) -> {
if (direction == EntitySearchDirection.FROM) {
relationInfo.setToName(entityName);
} else {
relationInfo.setFromName(entityName);
}
};
return Futures.transform(findByQuery(tenantId, query),
relations -> relations.stream()
.map(relation -> fetchRelationInfo(tenantId, relation, entityIdGetter, entityNameSetter))
.toList(), directExecutor());
}
private EntityRelationInfo fetchRelationInfo(TenantId tenantId, EntityRelation relation,
Function<EntityRelation, EntityId> entityIdGetter,
BiConsumer<EntityRelationInfo, String> entityNameSetter) {
var relationInfo = new EntityRelationInfo(relation);
entityNameSetter.accept(relationInfo, entityService.fetchEntityName(tenantId, entityIdGetter.apply(relation)).orElse("N/A"));
return relationInfo;
}
@Override
@ -495,15 +484,14 @@ public class BaseRelationService implements RelationService {
return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit);
}
protected void validate(EntityRelation relation) {
private static void validate(EntityRelation relation) {
if (relation == null) {
throw new DataValidationException("Relation type should be specified!");
throw new DataValidationException("Validation error: relation must not be null");
}
ConstraintValidator.validateFields(relation);
validate(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
}
protected void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
private static void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
validateType(type);
validateTypeGroup(typeGroup);
if (from == null) {
@ -514,25 +502,25 @@ public class BaseRelationService implements RelationService {
}
}
private void validateType(String type) {
if (StringUtils.isEmpty(type)) {
private static void validateType(String type) {
if (type == null) {
throw new DataValidationException("Relation type should be specified!");
}
}
private void validateTypeGroup(RelationTypeGroup typeGroup) {
private static void validateTypeGroup(RelationTypeGroup typeGroup) {
if (typeGroup == null) {
throw new DataValidationException("Relation type group should be specified!");
}
}
protected void validate(EntityId entity) {
private static void validate(EntityId entity) {
if (entity == null) {
throw new DataValidationException("Entity should be specified!");
}
}
private boolean matchFilters(List<RelationEntityTypeFilter> filters, EntityRelation relation, EntitySearchDirection direction) {
private static boolean matchFilters(List<RelationEntityTypeFilter> filters, EntityRelation relation, EntitySearchDirection direction) {
for (RelationEntityTypeFilter filter : filters) {
if (match(filter, relation, direction)) {
return true;
@ -541,7 +529,7 @@ public class BaseRelationService implements RelationService {
return false;
}
private boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) {
private static boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) {
if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) {
if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) {
return true;
@ -556,6 +544,7 @@ public class BaseRelationService implements RelationService {
@RequiredArgsConstructor
private static class RelationQueueCtx {
final SettableFuture<Set<EntityRelation>> future = SettableFuture.create();
final Set<EntityRelation> result = ConcurrentHashMap.newKeySet();
final Queue<RelationTask> tasks = new ConcurrentLinkedQueue<>();
@ -569,12 +558,7 @@ public class BaseRelationService implements RelationService {
}
@RequiredArgsConstructor
private static class RelationTask {
private final int currentLvl;
private final EntityId root;
private final List<EntityRelation> prevRelations;
}
private record RelationTask(int currentLvl, EntityId root, List<EntityRelation> prevRelations) {}
private void processQueue(RelationQueueCtx ctx) {
RelationTask task = ctx.tasks.poll();
@ -648,4 +632,5 @@ public class BaseRelationService implements RelationService {
handleEvictEvent(event);
}
}
}

Loading…
Cancel
Save