diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 42040a1acb..320d3e5bdd 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -20,11 +20,29 @@ UPDATE tenant_profile SET profile_data = jsonb_set( profile_data, '{configuration}', - (profile_data -> 'configuration') || '{ - "minAllowedScheduledUpdateIntervalInSecForCF": 3600 - }'::jsonb, + (profile_data -> 'configuration') + || jsonb_strip_nulls( + jsonb_build_object( + 'minAllowedScheduledUpdateIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(3600) + END, + 'maxRelationLevelPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + THEN NULL + ELSE to_jsonb(10) + END + ) + ), false ) -WHERE (profile_data -> 'configuration' -> 'minAllowedScheduledUpdateIntervalInSecForCF') IS NULL; +WHERE NOT ( + (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + AND + (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + ); -- UPDATE TENANT PROFILE CONFIGURATION END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 1d38480b91..f2265b3697 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -61,7 +61,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.function.BiConsumer; -import java.util.function.Function; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -359,7 +358,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (existingTask != null) { existingTask.cancel(false); String reason = cfDeleted ? "deletion" : "update"; - log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF " + reason + "!", tenantId, cfId); + log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason); } } @@ -400,9 +399,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware for (var linkProto : linksList) { var link = fromProto(linkProto); var cf = calculatedFields.get(link.cfId()); - applyToTargetCfEntityActors(link, callback, - cb -> new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback), - this::linkedTelemetryMsgForEntity); + withTargetEntities(link.entityId(), callback, (ids, cb) -> { + var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb); + ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg)); + }); } } @@ -594,48 +594,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void applyToTargetCfEntityActors(CalculatedFieldCtx calculatedFieldCtx, + private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx, TbCallback callback, BiConsumer action) { - if (isProfileEntity(calculatedFieldCtx.getEntityId().getEntityType())) { - var ids = entityProfileCache.getEntityIdsByProfileId(calculatedFieldCtx.getEntityId()); - if (ids.isEmpty()) { - callback.onSuccess(); - return; - } - var multiCallback = new MultipleTbCallback(ids.size(), callback); - ids.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - action.accept(id, multiCallback); - } - }); - return; - } - if (isMyPartition(calculatedFieldCtx.getEntityId(), callback)) { - action.accept(calculatedFieldCtx.getEntityId(), callback); - } + withTargetEntities(ctx.getEntityId(), callback, (ids, cb) -> ids.forEach(id -> action.accept(id, cb))); } - private void applyToTargetCfEntityActors(CalculatedFieldEntityCtxId link, TbCallback callback, - Function messageFactory, BiConsumer action) { - if (isProfileEntity(link.entityId().getEntityType())) { - var ids = entityProfileCache.getEntityIdsByProfileId(link.entityId()); + private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer, TbCallback> consumer) { + if (isProfileEntity(entityId.getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(entityId); if (ids.isEmpty()) { - callback.onSuccess(); + parentCallback.onSuccess(); return; } - var multiCallback = new MultipleTbCallback(ids.size(), callback); - var msg = messageFactory.apply(multiCallback); - ids.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - action.accept(id, msg); - } - }); + var multiCallback = new MultipleTbCallback(ids.size(), parentCallback); + var profileEntityIds = ids.stream().filter(id -> isMyPartition(id, multiCallback)).toList(); + if (profileEntityIds.isEmpty()) { + return; + } + consumer.accept(profileEntityIds, multiCallback); return; } - if (isMyPartition(link.entityId(), callback)) { - var msg = messageFactory.apply(callback); - action.accept(link.entityId(), msg); + if (isMyPartition(entityId, parentCallback)) { + consumer.accept(List.of(entityId), parentCallback); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 29f4daa783..b9968aefa9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -162,6 +162,8 @@ public class SystemInfoController extends BaseController { } systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); + systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); + systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index 509bb46c60..3794764351 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; @Data @@ -76,17 +75,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { } private Map toZones(Map entityIdKvEntryMap) { - return entityIdKvEntryMap.entrySet().stream().map(entry -> { - try { - if (entry.getValue().getJsonValue().isEmpty()) { - return null; - } - return Map.entry(entry.getKey(), new GeofencingZoneState(entry.getKey(), entry.getValue())); - } catch (Exception e) { - log.error("Failed to parse geofencing zone perimeter for entity id: {}", entry.getKey(), e); - return null; - } - }).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return entityIdKvEntryMap.entrySet().stream() + .filter(entry -> entry.getValue().getJsonValue().isPresent()) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> new GeofencingZoneState(entry.getKey(), entry.getValue()))); } private boolean updateZone(Map.Entry zoneEntry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index e44829b3a0..ad53b44d62 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -24,10 +24,11 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldException; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -40,10 +41,10 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; @Data @Slf4j @@ -130,8 +131,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { ZoneGroupConfiguration zoneGroupCfg = zoneGroups.get(argumentKey); if (zoneGroupCfg == null) { - log.error("[{}][{}] Zone group config is missing for the {}", entityId, ctx.getCalculatedField().getId(), argumentKey); - return; + throw new RuntimeException("Zone group configuration is missing for the: " + entityId); } boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones(); List zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java index dff9a4d9ea..d7794466a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java @@ -16,8 +16,9 @@ package org.thingsboard.server.service.cf.ctx.state; import jakarta.annotation.Nullable; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, - GeofencingPresenceStatus status) {} + GeofencingPresenceStatus status) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index d6475cc47f..bdedb8940c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -20,16 +20,16 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; @Data public class GeofencingZoneState { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 4d51e8096b..337d42fff4 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,7 +18,7 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 8df1d04f2e..e2359fff0c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -41,6 +40,7 @@ import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicS import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -58,9 +58,9 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index a7204f259f..ef481375c7 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -26,12 +26,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; @@ -58,9 +58,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldStateTest { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 87ef9bf0a1..7b086f1ce8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -171,10 +171,11 @@ public class GeofencingValueArgumentEntryTest { } @Test - void testNotParsableToPerimeterJsonKvEntryResultInEmptyArgument() { + void testNotParsableToPerimeterJsonKvEntryResultInExceptionTrowed() { BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); - GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)); - assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("The given string value cannot be transformed to Json object: \"{}\""); } } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index 3a6d0fa30d..f11e37921a 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -25,10 +25,10 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.ENTERED; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.LEFT; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.LEFT; public class GeofencingZoneStateTest { diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index ac6d45ad47..bd2111e834 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.utils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index fe3eb4e4d8..f83a812529 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -38,5 +38,7 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + int minAllowedScheduledUpdateIntervalInSecForCF; + int maxRelationLevelPerCfArgument; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 2fe554b801..972a3e0ee9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index ac8bfb691d..4e9b4252c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -25,7 +24,6 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.common.data.util.CollectionsUtil; import java.util.Collections; import java.util.List; @@ -48,9 +46,6 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami if (maxLevel < 1) { throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); } - if (maxLevel > 2) { - throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be greater than 2!"); - } if (direction == null) { throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); } @@ -64,6 +59,13 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami return maxLevel == 1; } + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (maxLevel > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { if (isSimpleRelation()) { throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index afc8402722..7818f2f5b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -38,11 +38,7 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca void setTimeUnit(TimeUnit timeUnit); - @Override - default void validate() { - if (!isScheduledUpdateEnabled()) { - return; - } + default void validate(long minAllowedScheduledUpdateInterval) { var timeUnit = getTimeUnit(); if (timeUnit == null) { throw new IllegalArgumentException("Scheduled update time unit should be specified!"); @@ -51,5 +47,9 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca throw new IllegalArgumentException("Unsupported scheduled update time unit: " + timeUnit + ". Allowed: " + SUPPORTED_TIME_UNITS); } + if (timeUnit.toSeconds(getScheduledUpdateInterval()) < minAllowedScheduledUpdateInterval) { + throw new IllegalArgumentException("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval); + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java similarity index 87% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index b009142f37..b797f91c68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; -import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; import java.util.HashMap; @@ -70,7 +72,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public void validate() { - ScheduledUpdateSupportedCalculatedFieldConfiguration.super.validate(); if (entityCoordinates == null) { throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java index ca3b91baec..a6ee0cfcd6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public sealed interface GeofencingEvent permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java index 3e88744132..38977cb650 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; import lombok.Getter; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java index 774e725650..a7937bb93c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public enum GeofencingReportStrategy { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java similarity index 90% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java index edd747587e..d7cf996fa7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public enum GeofencingTransitionEvent implements GeofencingEvent { ENTERED, LEFT diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java index 891940bf08..7ac87db10d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.geofencing; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.AttributeScope; @@ -22,12 +23,12 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CfArgumentDynamicSourceConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @Data +@JsonInclude(JsonInclude.Include.NON_NULL) public class ZoneGroupConfiguration { @Nullable @@ -65,6 +66,9 @@ public class ZoneGroupConfiguration { if (direction == null) { throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); } + if (hasDynamicSource()) { + refDynamicSourceConfiguration.validate(); + } } public boolean hasDynamicSource() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 7c174b005b..4c8b9e06bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -174,6 +174,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxArgumentsPerCF = 10; @Schema(example = "3600") private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + @Schema(example = "10") + private int maxRelationLevelPerCfArgument = 10; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java index 1fb55673d1..86fa52ba66 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -67,15 +67,34 @@ public class RelationQueryDynamicSourceConfigurationTest { } @Test - void validateShouldThrowWhenMaxLevelGreaterThanTwo() { + void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 2; + int argumentMaxRelationLevel = 3; + var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(3); + cfg.setMaxLevel(argumentMaxRelationLevel); cfg.setDirection(EntitySearchDirection.FROM); cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - assertThatThrownBy(cfg::validate) + String testRelationArgument = "testRelationArgument"; + assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relation query dynamic source configuration max relation level can't be greater than 2!"); + .hasMessage("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument); + } + + @Test + void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 5; + int argumentMaxRelationLevel = 2; + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(argumentMaxRelationLevel); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + String testRelationArgument = "testRelationArgument"; + assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException(); } @Test diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..e31ee97a58 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +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.EnumSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; + +@ExtendWith(MockitoExtension.class) +class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { + int scheduledUpdateInterval = 60; + int minAllowedInterval = (int) timeUnit.toSeconds(scheduledUpdateInterval - 1); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(scheduledUpdateInterval); + cfg.setTimeUnit(timeUnit); + + if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { + assertThatCode(() -> cfg.validate(minAllowedInterval)).doesNotThrowAnyException(); + return; + } + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported scheduled update time unit: " + timeUnit + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + cfg.setTimeUnit(null); + + assertThatThrownBy(() -> cfg.validate(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update time unit should be specified!"); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + int minAllowedInterval = (int) TimeUnit.HOURS.toSeconds(2); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(1); + cfg.setTimeUnit(TimeUnit.HOURS); + + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedInterval); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java similarity index 77% rename from common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java rename to common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 90d2768608..9031474388 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; 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.EnumSource; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; -import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import java.util.List; import java.util.Map; @@ -36,7 +35,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; @@ -126,46 +124,6 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB, never()).validate(); } - @Test - void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(60); - var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); - cfg.setZoneGroups(List.of(zg)); - cfg.setTimeUnit(null); - - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Scheduled update time unit should be specified!"); - } - - @ParameterizedTest - @EnumSource(TimeUnit.class) - void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(60); - var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); - cfg.setZoneGroups(List.of(zg)); - cfg.setEntityCoordinates(mock(EntityCoordinates.class)); - cfg.setTimeUnit(timeUnit); - - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - - if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { - assertThatCode(cfg::validate).doesNotThrowAnyException(); - return; - } - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported scheduled update time unit: " + timeUnit + - ". Allowed: " + SUPPORTED_TIME_UNITS); - } - - @Test void scheduledUpdateDisabledWhenIntervalIsZero() { var cfg = new GeofencingCalculatedFieldConfiguration(); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java index f3c1ae3263..988995ddbe 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -31,7 +31,7 @@ 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.Mockito.mock; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; public class ZoneGroupConfigurationTest { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 4e84cdebe1..c0cb886747 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,7 +37,6 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; -import java.util.concurrent.TimeUnit; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -80,7 +78,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); - updatedSchedulingConfiguration(calculatedField); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) @@ -94,23 +91,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } } - private void updatedSchedulingConfiguration(CalculatedField calculatedField) { - if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration configuration) { - if (!configuration.isScheduledUpdateEnabled()) { - return; - } - TimeUnit timeUnit = configuration.getTimeUnit(); - long intervalInSeconds = timeUnit.toSeconds(configuration.getScheduledUpdateInterval()); - int tenantProfileMinAllowedSecValue = tbTenantProfileCache.get(calculatedField.getTenantId()) - .getDefaultProfileConfiguration() - .getMinAllowedScheduledUpdateIntervalInSecForCF(); - if (intervalInSeconds < tenantProfileMinAllowedSecValue) { - configuration.setScheduledUpdateInterval(tenantProfileMinAllowedSecValue); - configuration.setTimeUnit(TimeUnit.SECONDS); - } - } - } - @Override public CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { log.trace("Executing findById, tenantId [{}], calculatedFieldId [{}]", tenantId, calculatedFieldId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 296aeff13a..05b782c26c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; @@ -28,6 +28,9 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import java.util.Map; +import java.util.stream.Collectors; + @Component public class CalculatedFieldDataValidator extends DataValidator { @@ -38,10 +41,22 @@ public class CalculatedFieldDataValidator extends DataValidator private ApiLimitService apiLimitService; @Override - protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { - validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); + protected void validateDataImpl(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfArgumentsPerCF(tenantId, calculatedField); validateCalculatedFieldConfiguration(calculatedField); + validateSchedulingConfiguration(tenantId, calculatedField); + validateRelationQuerySourceArguments(tenantId, calculatedField); + } + + @Override + protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { + long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); + if (maxCFsPerEntity <= 0) { + return; + } + if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) { + throw new DataValidationException("Calculated fields per entity limit reached!"); + } } @Override @@ -50,21 +65,9 @@ public class CalculatedFieldDataValidator extends DataValidator if (old == null) { throw new DataValidationException("Can't update non existing calculated field!"); } - validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateCalculatedFieldConfiguration(calculatedField); return old; } - private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) { - long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); - if (maxCFsPerEntity <= 0) { - return; - } - if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) { - throw new DataValidationException("Calculated fields per entity limit reached!"); - } - } - private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { return; @@ -79,8 +82,37 @@ public class CalculatedFieldDataValidator extends DataValidator } private void validateCalculatedFieldConfiguration(CalculatedField calculatedField) { + wrapAsDataValidation(calculatedField.getConfiguration()::validate); + } + + private void validateSchedulingConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledUpdateCfg) + || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { + return; + } + long minAllowedScheduledUpdateInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF); + wrapAsDataValidation(() -> scheduledUpdateCfg.validate(minAllowedScheduledUpdateInterval)); + } + + private void validateRelationQuerySourceArguments(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + .stream() + .filter(entry -> entry.getValue().hasDynamicSource()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); + if (relationQueryBasedArguments.isEmpty()) { + return; + } + int maxRelationLevel = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelationLevelPerCfArgument); + relationQueryBasedArguments.forEach((argumentName, relationQueryDynamicSourceConfiguration) -> + wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); + } + + private static void wrapAsDataValidation(Runnable validation) { try { - calculatedField.getConfiguration().validate(); + validation.run(); } catch (IllegalArgumentException e) { throw new DataValidationException(e.getMessage(), e); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 0d70e22339..1310b6c0b3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -28,13 +28,13 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -50,7 +50,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -148,7 +148,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldClampScheduledIntervalToTenantMin() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -181,22 +181,47 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setConfigurationVersion(0); cf.setConfiguration(cfg); - CalculatedField saved = calculatedFieldService.save(cf); + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: "); + } - assertThat(saved).isNotNull(); - assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); - var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); - // Assert: the interval is clamped up to tenant profile min - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); - int min = tbTenantProfileCache.get(tenantId) - .getDefaultProfileConfiguration() - .getMinAllowedScheduledUpdateIntervalInSecForCF(); - assertThat(savedInterval).isEqualTo(min); + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(Integer.MAX_VALUE); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Max relation level is greater than configured maximum allowed relation level in tenant profile"); } @Test diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index bf6ac7cf20..1693ee2763 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -38,6 +37,7 @@ import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicS import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; @@ -61,7 +61,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.thingsboard.server.common.data.AttributeScope.SERVER_SCOPE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin;