Browse Source

Merge branch 'lts-4.3' into release-4.3

release-4.3
Viacheslav Klimov 3 weeks ago
parent
commit
1bc6ad48b6
Failed to extract signature
  1. 1
      TEST_FAST.md
  2. 106
      application/pom.xml
  3. 3
      application/src/main/data/json/system/widget_bundles/cards.json
  4. 3
      application/src/main/data/json/system/widget_bundles/html_widgets.json
  5. 52
      application/src/main/data/json/system/widget_types/html_container.json
  6. 25
      application/src/main/data/upgrade/lts/schema_update.sql
  7. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  8. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  9. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  10. 871
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  11. 51
      application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java
  12. 4
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  13. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  14. 4
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  15. 259
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  16. 2
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  17. 46
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  18. 24
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  19. 24
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  20. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  21. 18
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  22. 162
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  23. 4
      application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
  24. 350
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  25. 24
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  26. 41
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  27. 28
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  28. 65
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  29. 30
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  30. 28
      application/src/main/java/org/thingsboard/server/controller/DomainController.java
  31. 42
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  32. 15
      application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java
  33. 58
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  34. 168
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  35. 46
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  36. 19
      application/src/main/java/org/thingsboard/server/controller/EventController.java
  37. 4
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  38. 4
      application/src/main/java/org/thingsboard/server/controller/MailConfigTemplateController.java
  39. 6
      application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java
  40. 4
      application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
  41. 41
      application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java
  42. 4
      application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java
  43. 37
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  44. 30
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  45. 2
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  46. 8
      application/src/main/java/org/thingsboard/server/controller/QueueController.java
  47. 17
      application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java
  48. 16
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  49. 15
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  50. 32
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  51. 40
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  52. 19
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  53. 110
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  54. 18
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  55. 21
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  56. 9
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  57. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  58. 80
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  59. 30
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  60. 31
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  61. 6
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  62. 3
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  63. 8
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  64. 13
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  65. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  66. 126
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  67. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  68. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  69. 12
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  70. 12
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  71. 38
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  72. 43
      application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java
  73. 37
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  74. 36
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  75. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  76. 105
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java
  77. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java
  78. 63
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java
  79. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  80. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  81. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java
  82. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  83. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java
  84. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  85. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  86. 67
      application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java
  87. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java
  88. 6
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  89. 1
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  90. 92
      application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java
  91. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  92. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  93. 145
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  94. 7
      application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
  95. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  96. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  97. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  98. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  99. 35
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  100. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

1
TEST_FAST.md

@ -10,6 +10,7 @@ export SUREFIRE_JAVA_OPTS="-Xmx1200m -Xss256k -XX:+ExitOnOutOfMemoryError"
mvn clean install -T6 -DskipTests -Dpkg.skip=true
mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4
mvn test -pl='msa/js-executor'
mvn test -pl dao -Dparallel=packages -DforkCount=4
mvn test -pl application -Dtest='!**/nosql/**,org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5

106
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.1.1</version>
<version>4.3.1.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>
@ -289,6 +289,11 @@
<artifactId>rest-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thingsboard.client</groupId>
<artifactId>thingsboard-ce-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
@ -502,6 +507,105 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>openapi-spec</id>
<properties>
<pkg.disabled>true</pkg.disabled>
<pkg.package.phase>none</pkg.package.phase>
<pkg.process-resources.phase>none</pkg.process-resources.phase>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.thingsboard.server.ThingsboardServerApplication</mainClass>
<jmxPort>9001</jmxPort>
</configuration>
<executions>
<execution>
<id>build-info</id>
<goals><goal>build-info</goal></goals>
<configuration>
<skip>false</skip>
</configuration>
</execution>
<execution>
<id>openapi-start</id>
<goals><goal>start</goal></goals>
<configuration>
<skip>false</skip>
<useTestClasspath>true</useTestClasspath>
<jvmArguments>-Xmx1024m</jvmArguments>
<arguments>
<argument>--spring.config.name=thingsboard</argument>
<argument>--spring.profiles.active=openapi</argument>
</arguments>
<maxAttempts>180</maxAttempts>
<wait>2000</wait>
</configuration>
</execution>
<execution>
<id>openapi-stop</id>
<goals><goal>stop</goal></goals>
<configuration>
<skip>false</skip>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<id>generate-openapi-spec</id>
<goals><goal>generate</goal></goals>
</execution>
</executions>
<configuration>
<apiDocsUrl>http://localhost:8080/v3/api-docs/thingsboard</apiDocsUrl>
<outputFileName>openapi.json</outputFileName>
<outputDir>${project.build.directory}</outputDir>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>verify-openapi-spec-generated</id>
<phase>verify</phase>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireFilesExist>
<files>
<file>${project.build.directory}/openapi.json</file>
</files>
<message>OpenAPI spec was not generated — target/openapi.json is missing. The springdoc-openapi-maven-plugin logs HTTP failures but does not fail the build; scan the log above for "An error has occured" or a 5xx response from /v3/api-docs/thingsboard and fix the underlying issue (e.g. duplicate @Schema names rejected by SwaggerConfiguration).</message>
</requireFilesExist>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>jenkins</id>

3
application/src/main/data/json/system/widget_bundles/cards.json

@ -24,6 +24,7 @@
"cards.html_value_card",
"cards.markdown_card",
"cards.simple_card",
"unread_notifications"
"unread_notifications",
"html_container"
]
}

3
application/src/main/data/json/system/widget_bundles/html_widgets.json

@ -11,6 +11,7 @@
"widgetTypeFqns": [
"cards.html_card",
"cards.html_value_card",
"cards.markdown_card"
"cards.markdown_card",
"html_container"
]
}

52
application/src/main/data/json/system/widget_types/html_container.json

File diff suppressed because one or more lines are too long

25
application/src/main/data/upgrade/lts/schema_update.sql

@ -0,0 +1,25 @@
--
-- Copyright © 2016-2026 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.
--
-- LTS cumulative schema update file.
-- All statements must be idempotent (use IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DO $$ ... END $$ guards, etc.).
-- This file is executed by SystemPatchApplier on every version increase within the LTS family.
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -36,6 +36,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService;
@ -691,6 +692,10 @@ public class ActorSystemContext {
@Getter
private MqttClientSettings mqttClientSettings;
@Autowired(required = false)
@Getter
private TbHttpClientSettings tbHttpClientSettings;
@Getter
@Setter
private TbActorSystem actorSystem;

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -485,7 +485,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) {
state.setCtx(ctx, actorCtx);
state.init(false);
if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) {
GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state;
@ -494,6 +493,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
Map<String, ArgumentEntry> arguments = fetchArguments(ctx);
state.update(arguments, ctx);
state.init(false);
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
states.put(ctx.getCfId(), state);

6
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
@ -1068,6 +1069,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getMqttClientSettings();
}
@Override
public TbHttpClientSettings getTbHttpClientSettings() {
return mainCtx.getTbHttpClientSettings();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());

871
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

File diff suppressed because it is too large

51
application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2026 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.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
@TbRuleEngineComponent
@Component
public class TbHttpClientSettingsComponent implements TbHttpClientSettings {
@Value("${actors.rule.external.http_client.max_parallel_requests:0}")
private int maxParallelRequests;
@Value("${actors.rule.external.http_client.max_pending_requests:0}")
private int maxPendingRequests;
@Value("${actors.rule.external.http_client.pool_max_connections:0}")
private int poolMaxConnections;
@Override
public int getMaxParallelRequests() {
return maxParallelRequests;
}
@Override
public int getMaxPendingRequests() {
return maxPendingRequests;
}
@Override
public int getPoolMaxConnections() {
return poolMaxConnections;
}
}

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

@ -417,7 +417,7 @@ public class AdminController extends BaseController {
"provider sends authorization code to specified redirect uri.)")
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/mail/oauth2/authorize", produces = "application/text")
public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
public String getMailOAuth2AuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
String state = StringUtils.generateSafeToken();
if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
CookieUtils.addCookie(response, PREV_URI_COOKIE_NAME, request.getParameter(PREV_URI_PATH_PARAMETER), 180);
@ -442,7 +442,7 @@ public class AdminController extends BaseController {
}
@GetMapping(value = "/mail/oauth2/code", params = {"code", "state"})
public void codeProcessingUrl(
public void handleMailOAuth2Callback(
@RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {
Optional<Cookie> prevUrlOpt = CookieUtils.getCookie(request, PREV_URI_COOKIE_NAME);

2
application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java

@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
@ -77,6 +78,7 @@ public class AlarmCommentController extends BaseController {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
alarmComment.setType(AlarmCommentType.OTHER);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}

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

@ -227,12 +227,12 @@ public class AlarmController extends BaseController {
return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser());
}
@ApiOperation(value = "Get Alarms (getAlarms)",
@ApiOperation(value = "Get Alarms (getAlarmsByEntity)",
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/alarm/{entityType}/{entityId}")
public PageData<AlarmInfo> getAlarms(
public PageData<AlarmInfo> getAlarmsByEntity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

259
application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java

@ -0,0 +1,259 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
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.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldFilter;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class AlarmRuleController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
public static final String ALARM_RULE_ID = "alarmRuleId";
private static final String TEST_SCRIPT_EXPRESSION =
"Execute the alarm rule TBEL condition expression and return the result. " +
"Alarm rule expressions must return a boolean value. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"expression\": \"return temperature > 50;\",\n" +
" \"arguments\": {\n" +
" \"temperature\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 55 }\n" +
" }\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n Expected result JSON contains \"output\" and \"error\".";
@ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)",
notes = "Creates or Updates the Alarm Rule. When creating alarm rule, platform generates Alarm Rule Id as " + UUID_WIKI_LINK +
"The newly created Alarm Rule Id will be present in the response. " +
"Specify existing Alarm Rule Id to update the alarm rule. " +
"Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. "
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/alarm/rule")
public AlarmRuleDefinition saveAlarmRule(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.")
@RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception {
alarmRuleDefinition.setTenantId(getTenantId());
checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
if (alarmRuleDefinition.getId() != null) {
checkAlarmRule(alarmRuleDefinition.getId());
}
CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField();
checkReferencedEntities(calculatedField.getConfiguration());
CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser());
return AlarmRuleDefinition.fromCalculatedField(saved);
}
@ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)",
notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/alarm/rule/{alarmRuleId}")
public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
return AlarmRuleDefinition.fromCalculatedField(calculatedField);
}
@ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)",
notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules/{entityType}/{entityId}")
public PageData<AlarmRuleDefinition> getAlarmRulesByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
checkParameter("entityId", entityIdStr);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr);
checkEntityId(entityId, Operation.READ_CALCULATED_FIELD);
PageData<CalculatedField> result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink));
return result.mapData(AlarmRuleDefinition::fromCalculatedField);
}
@ApiOperation(value = "Get alarm rules (getAlarmRules)",
notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules")
public PageData<AlarmRuleDefinitionInfo> getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Entity type filter. If not specified, alarm rules for all supported entity types will be returned.")
@RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, alarm rules for entity type filter will be returned.")
@RequestParam(required = false) Set<UUID> entities,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream()
.filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
} else {
entityTypes = EnumSet.of(entityType);
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.types(EnumSet.of(CalculatedFieldType.ALARM))
.entityTypes(entityTypes)
.entityIds(entities)
.build();
PageData<CalculatedFieldInfo> result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink);
return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo);
}
@ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)",
notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules/names")
public PageData<String> getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder);
return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink);
}
@ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)",
notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/alarm/rule/{alarmRuleId}")
@ResponseStatus(HttpStatus.OK)
public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
tbCalculatedFieldService.delete(calculatedField, getCurrentUser());
}
@ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)",
notes = "Gets latest alarm rule debug event for specified alarm rule id. " +
"Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/alarm/rule/{alarmRuleId}/debug")
public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
TenantId tenantId = getCurrentUser().getTenantId();
return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1))
.flatMap(events -> events.stream().map(EventInfo::getBody).findFirst())
.orElse(null);
}
@ApiOperation(value = "Test alarm rule TBEL expression (testAlarmRuleScript)",
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/alarm/rule/testScript")
public JsonNode testAlarmRuleScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null);
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException {
CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkNotNull(calculatedField);
if (calculatedField.getType() != CalculatedFieldType.ALARM) {
throw new ThingsboardException("Alarm rule not found", ThingsboardErrorCode.ITEM_NOT_FOUND);
}
return calculatedField;
}
}

2
application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java

@ -68,7 +68,7 @@ public class ApiKeyController extends BaseController {
private final ApiKeyService apiKeyService;
@ApiOperation(value = "Save API key for user (saveApiKey)",
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <value>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey {value}'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/apiKey")
public ApiKey saveApiKey(

46
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -23,6 +24,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -217,8 +219,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/assets")
public PageData<Asset> getTenantAssets(
@Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ -245,8 +246,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/assetInfos")
public PageData<AssetInfo> getTenantAssetInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,
@ -274,25 +274,30 @@ public class AssetController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Asset (getTenantAsset)",
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/assets", params = {"assetName"})
public Asset getTenantAsset(@RequestParam String assetName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
}
@ApiOperation(value = "Get Tenant Asset (getTenantAssetByName)",
notes = "Requested asset must be owned by tenant that the user belongs to. " +
"Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET)
@ResponseBody
public Asset getTenantAsset(
@GetMapping(value = "/tenant/asset")
public Asset getTenantAssetByName(
@Parameter(description = ASSET_NAME_DESCRIPTION)
@RequestParam String assetName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
return getTenantAsset(assetName);
}
@ApiOperation(value = "Get Customer Assets (getCustomerAssets)",
notes = "Returns a page of assets objects assigned to customer. " +
PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/assets")
public PageData<Asset> getCustomerAssets(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -324,8 +329,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/assetInfos")
public PageData<AssetInfo> getCustomerAssetInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -361,8 +365,7 @@ public class AssetController extends BaseController {
@ApiOperation(value = "Get Assets By Ids (getAssetsByIds)",
notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/assets")
public List<Asset> getAssetsByIds(
@Parameter(description = "A list of assets ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException, ExecutionException, InterruptedException {
@ -383,14 +386,14 @@ public class AssetController extends BaseController {
return checkNotNull(assets.get());
}
@ApiOperation(value = "Find related assets (findByQuery)",
@ApiOperation(value = "Find related assets (findAssetsByQuery)",
notes = "Returns all assets that are related to the specific entity. " +
"The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assets", method = RequestMethod.POST)
@ResponseBody
public List<Asset> findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
public List<Asset> findAssetsByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkNotNull(query.getAssetTypes());
@ -469,8 +472,7 @@ public class AssetController extends BaseController {
notes = "Returns a page of assets assigned to edge. " +
PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/assets")
public PageData<Asset> getEdgeAssets(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION)
@PathVariable(EDGE_ID) String strEdgeId,
@ -516,11 +518,11 @@ public class AssetController extends BaseController {
return checkNotNull(filteredResult);
}
@ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)",
@ApiOperation(value = "Import the bulk of assets (processAssetBulkImport)",
notes = "There's an ability to import the bulk of assets using the only .csv file.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@PostMapping("/asset/bulk_import")
public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
public BulkImportResult<Asset> processAssetBulkImport(@RequestBody BulkImportRequest request) throws Exception {
SecurityUser user = getCurrentUser();
return assetBulkImportService.processBulkImport(request, user);
}

24
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -175,8 +176,7 @@ public class AssetProfileController extends BaseController {
notes = "Returns a page of asset profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/assetProfiles")
public PageData<AssetProfile> getAssetProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -196,8 +196,7 @@ public class AssetProfileController extends BaseController {
notes = "Returns a page of asset profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/assetProfileInfos")
public PageData<AssetProfileInfo> getAssetProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -227,12 +226,10 @@ public class AssetProfileController extends BaseController {
return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly));
}
@ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)",
notes = "Requested asset profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"})
public List<AssetProfileInfo> getAssetProfilesByIds(
public List<AssetProfileInfo> getAssetProfilesByIdsV1(
@Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
@ -243,4 +240,15 @@ public class AssetProfileController extends BaseController {
return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds);
}
@ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)",
notes = "Requested asset profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/assetProfileInfos/list")
public List<AssetProfileInfo> getAssetProfilesByIds(
@Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException {
return getAssetProfilesByIdsV1(assetProfileUUIDs);
}
}

24
application/src/main/java/org/thingsboard/server/controller/AuditLogController.java

@ -18,11 +18,10 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
@ -50,6 +49,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PA
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION;
@ -73,8 +73,7 @@ public class AuditLogController extends BaseController {
"and users actions (login, logout, etc.) that belong to this customer. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/audit/logs/customer/{customerId}")
public PageData<AuditLog> getAuditLogsByCustomerId(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -106,8 +105,7 @@ public class AuditLogController extends BaseController {
"For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/audit/logs/user/{userId}")
public PageData<AuditLog> getAuditLogsByUserId(
@Parameter(description = USER_ID_PARAM_DESCRIPTION)
@PathVariable("userId") String strUserId,
@ -138,10 +136,9 @@ public class AuditLogController extends BaseController {
notes = "Returns a page of audit logs related to the actions on the targeted entity. " +
"Basically, this API call is used to get the full lifecycle of some specific entity. " +
"For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/audit/logs/entity/{entityType}/{entityId}")
public PageData<AuditLog> getAuditLogsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String strEntityType,
@ -173,10 +170,9 @@ public class AuditLogController extends BaseController {
@ApiOperation(value = "Get all audit logs (getAuditLogs)",
notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/audit/logs")
public PageData<AuditLog> getAuditLogs(
@Parameter(description = PAGE_SIZE_DESCRIPTION)
@RequestParam int pageSize,

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

@ -136,7 +136,7 @@ public class AuthController extends BaseController {
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/activate", params = {"activateToken"})
@GetMapping(value = "/noauth/activate")
public ResponseEntity<?> checkActivateToken(
@Parameter(description = "The activate token string.")
@RequestParam(value = "activateToken") String activateToken) {
@ -176,7 +176,7 @@ public class AuthController extends BaseController {
"If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
"If token is not valid, returns '409 Conflict'. " +
"If token is expired, redirects to error page.")
@GetMapping(value = "/noauth/resetPassword", params = {"resetToken"})
@GetMapping(value = "/noauth/resetPassword")
public ResponseEntity<?> checkResetToken(
@Parameter(description = "The reset token string.")
@RequestParam(value = "resetToken") String resetToken) {

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

@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInfo;
@ -681,6 +682,17 @@ public abstract class BaseController {
return entity;
}
protected void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
for (EntityId referencedEntityId : calculatedFieldConfig.getReferencedEntities()) {
EntityType refEntityType = referencedEntityId.getEntityType();
switch (refEntityType) {
case TENANT -> {}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'.");
}
}
}
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
return checkEntityId(deviceId, deviceService::findDeviceById, operation);
}
@ -881,10 +893,8 @@ public abstract class BaseController {
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
if (!user.isSystemAdmin()) {
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception {

162
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -15,15 +15,15 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
@ -36,13 +36,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -61,21 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
@ -94,17 +81,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class CalculatedFieldController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String CALCULATED_FIELD_ID = "calculatedFieldId";
public static final int TIMEOUT = 20;
private static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
@ -163,27 +146,17 @@ public class CalculatedFieldController extends BaseController {
return calculatedField;
}
@ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)",
notes = "Fetch the Calculated Fields based on the provided Entity Id."
)
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"})
public PageData<CalculatedField> getCalculatedFieldsByEntityId(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Calculated field type. If not specified, all types will be returned.")
@RequestParam(required = false) CalculatedFieldType type,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<CalculatedField> getCalculatedFieldsByEntityIdV1(@PathVariable("entityType") String entityType,
@PathVariable("entityId") String entityIdStr,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) CalculatedFieldType type,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
checkParameter("entityId", entityIdStr);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr);
@ -191,8 +164,29 @@ public class CalculatedFieldController extends BaseController {
return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink));
}
@ApiOperation(value = "Get Calculated Fields by Entity Id (getCalculatedFieldsByEntityId)",
notes = "Fetch the Calculated Fields based on the provided Entity Id."
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/calculatedField/{entityType}/{entityId}")
public PageData<CalculatedField> getCalculatedFieldsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page,
@Parameter(description = "Calculated field type. If not specified, all types will be returned.")
@RequestParam(required = false) CalculatedFieldType type,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException {
return getCalculatedFieldsByEntityIdV1(entityType, entityIdStr, pageSize, page, type, textSearch, sortProperty, sortOrder);
}
@ApiOperation(value = "Get calculated fields (getCalculatedFields)",
notes = "Fetch tenant calculated fields based on the filter.")
@Parameters({
@Parameter(name = "name", description = "Repeatable name query parameter", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/calculatedFields")
public PageData<CalculatedFieldInfo> getCalculatedFields(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -205,14 +199,13 @@ public class CalculatedFieldController extends BaseController {
@RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.")
@RequestParam(required = false) Set<UUID> entities,
@Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2")
@RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma)
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
@ -289,88 +282,11 @@ public class CalculatedFieldController extends BaseController {
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/calculatedField/testScript")
public JsonNode testScript(
public JsonNode testCalculatedFieldScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
getTenantId(),
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
}
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}
}
@RequestBody JsonNode inputParams) throws ThingsboardException {
checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null);
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
}

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

@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -81,8 +82,7 @@ public class ComponentDescriptorController extends BaseController {
notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " +
COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
@RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/components")
public List<ComponentDescriptor> getComponentDescriptorsByTypes(
@Parameter(description = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("componentTypes") String[] strComponentTypes,

350
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -444,106 +444,6 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":true,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempAttr\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value.";
protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'";
@ -1328,8 +1228,6 @@ public class ControllerConstants {
ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE;
protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
@ -1343,219 +1241,6 @@ public class ControllerConstants {
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" {\n" +
" \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" +
" \"alarmType\":\"Temperature Alarm\",\n" +
" \"clearRule\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"propagate\":false,\n" +
" \"createRules\":{\n" +
" \"MAJOR\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"COMPLEX\",\n" +
" \"operation\":\"OR\",\n" +
" \"predicates\":[\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS_OR_EQUAL\"\n" +
" },\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"WARNING\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"tempConstant\",\n" +
" \"type\":\"CONSTANT\"\n" +
" },\n" +
" \"value\":30,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":0.0,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":false,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempThreshold\"\n" +
" }\n" +
" },\n" +
" \"operation\":\"EQUAL\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"CRITICAL\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" }\n" +
" },\n" +
" \"propagateRelationTypes\":null\n" +
" }\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
@ -1577,40 +1262,11 @@ public class ControllerConstants {
" }\n" +
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE +
"Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
"Device profile data object contains device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
"First one is the default device profile data configuration and second one - the custom one. " +
NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE +
NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:";
protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE +
"Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " +
"**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " +
"**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE +
"## Specific Time Schedule" + NEW_LINE +
DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE +
"## Custom Schedule" +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE;
protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE +
"Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE +
"Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " +
"or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" +
"\n**'sourceType'** of the **'sourceAttribute'** can be: \n" +
" * 'CURRENT_DEVICE';\n" +
" * 'CURRENT_CUSTOMER';\n" +
" * 'CURRENT_TENANT'." + NEW_LINE +
"**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE +
"## Repeating alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE +
"## Duration alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE +
"**'unit'** can be: \n" +
" * 'SECONDS';\n" +
" * 'MINUTES';\n" +
" * 'HOURS';\n" +
" * 'DAYS'." + NEW_LINE;
protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE +
"There are 3 types of device provision configuration for the device profile: \n" +
" * 'DISABLED';\n" +
@ -1618,8 +1274,8 @@ public class ControllerConstants {
" * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE +
"Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE;
protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE +
KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION +
PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
protected static final String DEVICE_PROFILE_ID = "deviceProfileId";

24
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -172,8 +173,7 @@ public class CustomerController extends BaseController {
notes = "Returns a page of customers owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customers")
public PageData<Customer> getCustomers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -193,8 +193,7 @@ public class CustomerController extends BaseController {
@ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)",
notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/customers")
public Customer getTenantCustomer(
@Parameter(description = "A string value representing the Customer title.")
@RequestParam String customerTitle) throws ThingsboardException {
@ -202,12 +201,10 @@ public class CustomerController extends BaseController {
return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found");
}
@ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)",
notes = "Returns a list of Customer objects based on the provided ids." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customers", params = {"customerIds"})
public List<Customer> getCustomersByIds(
public List<Customer> getCustomersByIdsV1(
@Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
@ -218,4 +215,15 @@ public class CustomerController extends BaseController {
return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds);
}
@ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)",
notes = "Returns a list of Customer objects based on the provided ids." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customers/list")
public List<Customer> getCustomersByIds(
@Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException {
return getCustomersByIdsV1(customerUUIDs);
}
}

41
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -17,10 +17,10 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletResponse;
@ -119,7 +119,7 @@ public class DashboardController extends BaseController {
"Used to adjust view of the dashboards according to the difference between browser and server time.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/serverTime")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "1636023857137")))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "1636023857137")))
public long getServerTime() {
return System.currentTimeMillis();
}
@ -131,7 +131,7 @@ public class DashboardController extends BaseController {
"The actual value of the limit is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/maxDatapointsLimit")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", examples = @ExampleObject(value = "5000")))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "integer", format = "int64", example = "5000")))
public long getMaxDatapointsLimit() {
return maxDatapointsLimit;
}
@ -151,6 +151,8 @@ public class DashboardController extends BaseController {
@ApiOperation(value = "Get Dashboard (getDashboardById)",
notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = Dashboard.class)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/{dashboardId}")
public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@ -176,6 +178,8 @@ public class DashboardController extends BaseController {
"Referencing non-existing dashboard Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Dashboard entity. " +
TENANT_AUTHORITY_PARAGRAPH)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = Dashboard.class)))
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping(value = "/dashboard")
public void saveDashboard(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the dashboard.")
@ -325,12 +329,12 @@ public class DashboardController extends BaseController {
return tbDashboardService.unassignDashboardFromPublicCustomer(dashboard, getCurrentUser());
}
@ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)",
@ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboardsByTenantId)",
notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"})
public PageData<DashboardInfo> getTenantDashboards(
@GetMapping(value = "/tenant/{tenantId}/dashboards")
public PageData<DashboardInfo> getTenantDashboardsByTenantId(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -353,7 +357,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the tenant of a current user. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/dashboards", params = {"pageSize", "page"})
@GetMapping(value = "/tenant/dashboards")
public PageData<DashboardInfo> getTenantDashboards(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -380,7 +384,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects owned by the specified customer. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/dashboards")
public PageData<DashboardInfo> getCustomerDashboards(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -413,6 +417,8 @@ public class DashboardController extends BaseController {
"If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " +
"If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. "
+ DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = HomeDashboard.class)))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/home")
public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,
@ -574,7 +580,7 @@ public class DashboardController extends BaseController {
notes = "Returns a page of dashboard info objects assigned to the specified edge. "
+ DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"})
@GetMapping(value = "/edge/{edgeId}/dashboards")
public PageData<DashboardInfo> getEdgeDashboards(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@ -602,13 +608,10 @@ public class DashboardController extends BaseController {
return checkNotNull(filteredResult);
}
@ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)",
notes = "Returns a list of DashboardInfo objects based on the provided ids. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboards", params = {"dashboardIds"})
public List<DashboardInfo> getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
public List<DashboardInfo> getDashboardsByIdsV1(@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<DashboardId> dashboardIds = new ArrayList<>();
for (UUID dashboardUUID : dashboardUUIDs) {
@ -618,6 +621,16 @@ public class DashboardController extends BaseController {
return filterDashboardsByReadPermission(dashboards);
}
@ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)",
notes = "Returns a list of DashboardInfo objects based on the provided ids. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboards/list")
public List<DashboardInfo> getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
return getDashboardsByIdsV1(dashboardUUIDs);
}
private Set<CustomerId> customerIdFromStr(String[] strCustomerIds) {
Set<CustomerId> customerIds = new HashSet<>();
if (strCustomerIds != null) {

28
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@ -75,6 +76,7 @@ public class DeviceConnectivityController extends BaseController {
description = "OK",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = JsonNode.class),
examples = {
@ExampleObject(
name = "http",
@ -105,7 +107,18 @@ public class DeviceConnectivityController extends BaseController {
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
}
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.")
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)",
notes = "Download server certificate.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
schema = @Schema(type = "string", format = "binary")
)
)
})
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadServerCertificate(@Parameter(description = PROTOCOL_PARAM_DESCRIPTION)
@ -122,7 +135,18 @@ public class DeviceConnectivityController extends BaseController {
.body(pemCert);
}
@ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)", notes = "Download generated docker-compose.yml for gateway.")
@ApiOperation(value = "Download generated docker-compose.yml file for gateway (downloadGatewayDockerCompose)",
notes = "Download generated docker-compose.yml for gateway.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE,
schema = @Schema(type = "string", format = "binary")
)
)
})
@RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}/docker-compose/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadGatewayDockerCompose(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)

65
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -29,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -204,16 +206,16 @@ public class DeviceController extends BaseController {
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
"Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
"You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile ID</b> below: \n\n" +
"- Credentials type: **\"Access token\"** with **device profile ID** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile default</b> below: \n\n" +
"- Credentials type: **\"Access token\"** with **device profile default** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
"- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" +
"Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
"- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
@ -320,14 +322,14 @@ public class DeviceController extends BaseController {
"Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" +
"You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device ID</b> and with <b>device ID</b> below: \n\n" +
"- Credentials type: **\"Access token\"** with **device ID** and with **device ID** below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
"- Credentials type: **\"X509\"** with **device profile ID** below: \n\n" +
"Note: **credentialsId** - format **Sha3Hash**, **certificateValue** - format **PEM** (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
"- Credentials type: **\"MQTT_BASIC\"** with **device profile ID** below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"- You may find the example of **LwM2M** device and **RPK** credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Update to real value:\n" +
@ -350,8 +352,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/devices")
public PageData<Device> getTenantDevices(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -378,8 +379,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/deviceInfos")
public PageData<DeviceInfo> getTenantDeviceInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -411,25 +411,31 @@ public class DeviceController extends BaseController {
return checkNotNull(deviceService.findDeviceInfosByFilter(filter.build(), pageLink));
}
@ApiOperation(value = "Get Tenant Device (getTenantDevice)",
notes = "Requested device must be owned by tenant that the user belongs to. " +
"Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/tenant/devices", params = {"deviceName"})
public Device getTenantDevice(
@Parameter(description = DEVICE_NAME_DESCRIPTION)
@RequestParam String deviceName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(deviceService.findDeviceByTenantIdAndName(tenantId, deviceName));
}
@ApiOperation(value = "Get Tenant Device (getTenantDeviceByName)",
notes = "Requested device must be owned by tenant that the user belongs to. " +
"Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/device")
public Device getTenantDeviceByName(
@Parameter(description = DEVICE_NAME_DESCRIPTION)
@RequestParam String deviceName) throws ThingsboardException {
return getTenantDevice(deviceName);
}
@ApiOperation(value = "Get Customer Devices (getCustomerDevices)",
notes = "Returns a page of devices objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/devices")
public PageData<Device> getCustomerDevices(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -461,8 +467,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/customer/{customerId}/deviceInfos")
public PageData<DeviceInfo> getCustomerDeviceInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("customerId") String strCustomerId,
@ -502,8 +507,7 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/devices")
public List<Device> getDevicesByIds(
@Parameter(description = "A list of devices ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException, ExecutionException, InterruptedException {
@ -524,14 +528,14 @@ public class DeviceController extends BaseController {
return checkNotNull(devices.get());
}
@ApiOperation(value = "Find related devices (findByQuery)",
@ApiOperation(value = "Find related devices (findDevicesByQuery)",
notes = "Returns all devices that are related to the specific entity. " +
"The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", method = RequestMethod.POST)
@ResponseBody
public List<Device> findByQuery(
public List<Device> findDevicesByQuery(
@Parameter(description = "The device search query JSON")
@RequestBody DeviceSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
@ -730,8 +734,7 @@ public class DeviceController extends BaseController {
notes = "Returns a page of devices assigned to edge. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/edge/{edgeId}/devices")
public PageData<DeviceInfo> getEdgeDevices(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,

30
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -129,7 +130,7 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId()));
}
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
@ApiOperation(value = "Get time series keys (getDeviceProfileTimeseriesKeys)",
notes = "Get a set of unique time series keys used by devices that belong to specified profile. " +
"If profile is not set returns a list of unique keys among all profiles. " +
"The call is used for auto-complete in the UI forms. " +
@ -138,7 +139,7 @@ public class DeviceProfileController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET)
@ResponseBody
public List<String> getTimeseriesKeys(
public List<String> getDeviceProfileTimeseriesKeys(
@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
DeviceProfileId deviceProfileId;
@ -228,8 +229,7 @@ public class DeviceProfileController extends BaseController {
notes = "Returns a page of devices profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/deviceProfiles")
public PageData<DeviceProfile> getDeviceProfiles(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -249,8 +249,7 @@ public class DeviceProfileController extends BaseController {
notes = "Returns a page of devices profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/deviceProfileInfos")
public PageData<DeviceProfileInfo> getDeviceProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -282,14 +281,10 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly));
}
@ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfilesByIds)",
notes = "Requested device profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"})
public List<DeviceProfileInfo> getDeviceProfileInfosByIds(
@Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
public List<DeviceProfileInfo> getDeviceProfileInfosByIdsV1(@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<DeviceProfileId> deviceProfileIds = new ArrayList<>();
for (UUID deviceProfileUUID : deviceProfileUUIDs) {
@ -298,4 +293,15 @@ public class DeviceProfileController extends BaseController {
return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds);
}
@ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfileInfosByIds)",
notes = "Requested device profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/deviceProfileInfos/list")
public List<DeviceProfileInfo> getDeviceProfileInfosByIds(
@Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
return getDeviceProfileInfosByIdsV1(deviceProfileUUIDs);
}
}

28
application/src/main/java/org/thingsboard/server/controller/DomainController.java

@ -81,31 +81,31 @@ public class DomainController extends BaseController {
return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser());
}
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
@ApiOperation(value = "Update oauth2 clients (updateDomainOauth2Clients)",
notes = "Update oauth2 clients for the specified domain. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PutMapping(value = "/domain/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
public void updateDomainOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
DomainId domainId = new DomainId(id);
Domain domain = checkDomainId(domainId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
tbDomainService.updateOauth2Clients(domain, oAuth2ClientIds, getCurrentUser());
}
@ApiOperation(value = "Get Domain infos (getTenantDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Get Domain infos (getDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@GetMapping(value = "/domain/infos")
public PageData<DomainInfo> getTenantDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on domain's name")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<DomainInfo> getDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on domain's name")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return domainService.findDomainInfosByTenantId(getTenantId(), pageLink);

42
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -277,7 +278,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"})
@GetMapping(value = "/tenant/edgeInfos")
public PageData<EdgeInfo> getTenantEdgeInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -300,17 +301,24 @@ public class EdgeController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Edge (getTenantEdge)",
notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
"Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edges", params = {"edgeName"})
public Edge getTenantEdge(@Parameter(description = "Unique name of the edge", required = true)
@RequestParam String edgeName) throws ThingsboardException {
public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName));
}
@ApiOperation(value = "Get Tenant Edge by name (getTenantEdgeByName)",
notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
"Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/edge")
public Edge getTenantEdgeByName(@Parameter(description = "Unique name of the edge", required = true)
@RequestParam String edgeName) throws ThingsboardException {
return getTenantEdge(edgeName);
}
@ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)",
notes = "Change root rule chain of the edge to the new provided rule chain. \n" +
"This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
@ -334,7 +342,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/edges")
public PageData<Edge> getCustomerEdges(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -369,7 +377,7 @@ public class EdgeController extends BaseController {
notes = "Returns a page of edges info objects assigned to customer. " +
PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/edgeInfos")
public PageData<EdgeInfo> getCustomerEdgeInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable("customerId") String strCustomerId,
@ -400,12 +408,10 @@ public class EdgeController extends BaseController {
return checkNotNull(result);
}
@ApiOperation(value = "Get Edges By Ids (getEdgesByIds)",
notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edges", params = {"edgeIds"})
public List<Edge> getEdgesByIds(
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
checkArrayParameter("edgeIds", strEdgeIds);
SecurityUser user = getCurrentUser();
@ -425,13 +431,23 @@ public class EdgeController extends BaseController {
return checkNotNull(edges);
}
@ApiOperation(value = "Find related edges (findByQuery)",
@ApiOperation(value = "Get Edges By Ids (getEdgeList)",
notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edges/list")
public List<Edge> getEdgeList(
@Parameter(description = "A list of edges ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException, ExecutionException, InterruptedException {
return getEdgesByIds(strEdgeIds);
}
@ApiOperation(value = "Find related edges (findEdgesByQuery)",
notes = "Returns all edges that are related to the specific entity. " +
"The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/edges")
public List<Edge> findByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
public List<Edge> findEdgesByQuery(@RequestBody EdgeSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkNotNull(query.getEdgeTypes());

15
application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
@ -213,19 +214,19 @@ public class EntitiesVersionControlController extends BaseController {
" \"timestamp\": 1655198593000,\n" +
" \"id\": \"fd82625bdd7d6131cf8027b44ee967012ecaf990\",\n" +
" \"name\": \"Devices and assets - v2.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" +
" {\n" +
" \"timestamp\": 1655198528000,\n" +
" \"id\": \"682adcffa9c8a2f863af6f00c4850323acbd4219\",\n" +
" \"name\": \"Update my device\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" },\n" +
" {\n" +
" \"timestamp\": 1655198280000,\n" +
" \"id\": \"d2a6087c2b30e18cc55e7cdda345a8d0dfb959a4\",\n" +
" \"name\": \"Devices and assets - v1.0\",\n" +
" \"author\": \"John Doe <johndoe@gmail.com>\"\n" +
" \"author\": \"John Doe (johndoe@gmail.com)\"\n" +
" }\n" +
" ],\n" +
" \"totalPages\": 1,\n" +
@ -234,7 +235,7 @@ public class EntitiesVersionControlController extends BaseController {
"}" +
MARKDOWN_CODE_BLOCK_END +
TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{entityType}/{externalEntityUuid}", params = {"branch", "pageSize", "page"})
@GetMapping(value = "/version/{entityType}/{externalEntityUuid}")
public DeferredResult<PageData<EntityVersion>> listEntityVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType,
@Parameter(description = "A string value representing external entity id. This is `externalId` property of an entity, or otherwise if not set - simply id of this entity.")
@ -263,7 +264,7 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " +
"The response structure is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version/{entityType}", params = {"branch", "pageSize", "page"})
@GetMapping(value = "/version/{entityType}")
public DeferredResult<PageData<EntityVersion>> listEntityTypeVersions(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType,
@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true)
@ -288,7 +289,7 @@ public class EntitiesVersionControlController extends BaseController {
"If specified branch does not exist - empty page data will be returned. " +
"The response format is the same as for `listEntityVersions` API method." +
TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/version", params = {"branch", "pageSize", "page"})
@GetMapping(value = "/version")
public DeferredResult<PageData<EntityVersion>> listVersions(@Parameter(description = BRANCH_PARAM_DESCRIPTION, required = true)
@RequestParam String branch,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -355,7 +356,7 @@ public class EntitiesVersionControlController extends BaseController {
"Returns an object with current entity data and the one at a specific version. " +
"Entity data structure is the same as stored in a repository. " +
TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/diff/{entityType}/{internalEntityUuid}", params = {"versionId"})
@GetMapping(value = "/diff/{entityType}/{internalEntityUuid}")
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable EntityType entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

58
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -47,6 +48,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.Set;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
@ -115,9 +118,11 @@ public class EntityQueryController extends BaseController {
return entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
}
@Deprecated(forRemoval = true)
@ApiOperation(
value = "Find Available Entity Keys by Query",
value = "Find Available Entity Keys by Query (deprecated)",
notes = """
**Deprecated.** Use the V2 endpoint (`POST /api/v2/entitiesQuery/find/keys`) instead.\n
Returns unique time series and/or attribute key names from entities matching the query.\n
Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n
Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
@ -128,9 +133,6 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query,
// fixme: combination of timeseries = false and attributes = false is allowed, but always results in empty response, therefore does not make any sense
// such combinations should NOT be allowed, but changing this will break clients
@Parameter(description = """
When true, includes unique time series key names in the response.
When false, the 'timeseries' list will be empty.""")
@ -155,6 +157,54 @@ public class EntityQueryController extends BaseController {
return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope));
}
@ApiOperation(
value = "Find Available Entity Keys By Query",
notes = """
Discovers unique time series and/or attribute key names available on entities that match the given query.
Works in two steps: first, the request body (an Entity Data Query) is executed to find matching entities
(page size is capped at 100); then, all distinct key names are collected from those entities.\n
Optionally, each key can include a sample the most recent value (by timestamp) for that key
across all matched entities."""
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/v2/entitiesQuery/find/keys")
public DeferredResult<AvailableEntityKeysV2> findAvailableEntityKeysByQueryV2(
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query,
@Parameter(description = """
When true, includes unique time series keys in the response.
When false, the 'timeseries' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
@RequestParam(defaultValue = "true") boolean includeTimeseries,
@Parameter(description = """
When true, includes unique attribute keys in the response.
When false, the 'attributes' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""")
@RequestParam(defaultValue = "true") boolean includeAttributes,
@Parameter(description = """
Filters attribute keys by scope. Only applies when 'includeAttributes' is true.
When not specified, scopes are auto-determined: all three scopes (server, client, shared) for device entities,
server scope only for other entity types.""",
schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(required = false) Set<AttributeScope> scopes,
@Parameter(description = """
When true, each key entry includes a 'sample' object with the most recent value and timestamp.
When false, only key names are returned (sample is omitted from JSON).""")
@RequestParam(defaultValue = "false") boolean includeSamples
) throws ThingsboardException {
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE);
}
return wrapFuture(entityQueryService.findAvailableEntityKeysByQuery(
getCurrentUser(), query,
includeTimeseries, includeAttributes, scopes, includeSamples));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@PostMapping("/edqs/system/request")
public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) {

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

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -71,25 +73,22 @@ public class EntityRelationController extends BaseController {
"If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer.";
@ApiOperation(value = "Create Relation (saveRelation)",
notes = "Creates or updates a relation between two entities in the platform. " +
"Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
SECURITY_CHECKS_ENTITIES_DESCRIPTION)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping("/relation")
public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
@PostMapping(value = "/relation")
public void saveRelationV1(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
doSave(relation);
}
@ApiOperation(value = "Create Relation (saveRelationV2)",
@ApiOperation(value = "Create Relation (saveRelation)",
notes = "Creates or updates a relation between two entities in the platform. " +
"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')")
@PostMapping("/v2/relation")
public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
@PostMapping(value = "/v2/relation")
public EntityRelation saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
return doSave(relation);
}
@ -103,11 +102,10 @@ public class EntityRelationController extends BaseController {
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)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@DeleteMapping(value = "/relation")
public void deleteRelationV1(@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,
@ -116,16 +114,16 @@ public class EntityRelationController extends BaseController {
doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
}
@ApiOperation(value = "Delete Relation (deleteRelationV2)",
@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')")
@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 {
@DeleteMapping(value = "/v2/relation")
public EntityRelation 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,
@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);
}
@ -144,11 +142,11 @@ public class EntityRelationController extends BaseController {
return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete common relations (deleteCommonRelations)",
@ApiOperation(value = "Delete common relations (deleteRelations)",
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')")
@DeleteMapping(value = "/relations", params = {"entityId", "entityType"})
@DeleteMapping(value = "/relations")
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);
@ -161,7 +159,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')")
@GetMapping(value = "/relation", params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
@GetMapping(value = "/relation")
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,
@ -180,14 +178,11 @@ public class EntityRelationController extends BaseController {
return checkNotNull(relationService.getRelation(getTenantId(), fromId, toId, strRelationType, typeGroup));
}
@ApiOperation(value = "Get List of Relations (findByFrom)",
notes = "Returns list of relation objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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)
public List<EntityRelation> findByFrom(@RequestParam(FROM_ID) String strFromId,
@RequestParam(FROM_TYPE) String strFromType,
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType);
@ -197,9 +192,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByFrom(getTenantId(), entityId, typeGroup)));
}
@ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)",
notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@ApiOperation(value = "Get List of Relations (findEntityRelationsByFrom)",
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')")
@GetMapping(value = "/relations/from/{fromType}/{fromId}")
public List<EntityRelation> findEntityRelationsByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByFrom(strFromId, strFromType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@ -214,15 +219,24 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByFrom(getTenantId(), entityId, typeGroup).get()));
}
@ApiOperation(value = "Get List of Relations (findByFrom)",
notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByFrom)",
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')")
@GetMapping(value = "/relations/info/from/{fromType}/{fromId}")
public List<EntityRelationInfo> findEntityRelationInfosByFrom(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException {
return findInfoByFrom(strFromId, strFromType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
public List<EntityRelation> findByFrom(@RequestParam(FROM_ID) String strFromId,
@RequestParam(FROM_TYPE) String strFromType,
@RequestParam(RELATION_TYPE) String strRelationType,
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType);
@ -233,10 +247,21 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByFromAndType(getTenantId(), entityId, strRelationType, typeGroup)));
}
@ApiOperation(value = "Get List of Relations (findByTo)",
notes = "Returns list of relation objects for the specified entity by the 'to' direction. " +
@ApiOperation(value = "Get List of Relations (findEntityRelationsByFromAndRelationType)",
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')")
@GetMapping(value = "/relations/from/{fromType}/{fromId}/{relationType}")
public List<EntityRelation> findEntityRelationsByFromAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_TYPE) String strFromType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(FROM_ID) String strFromId,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByFrom(strFromId, strFromType, strRelationType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@ -250,9 +275,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByTo(getTenantId(), entityId, typeGroup)));
}
@ApiOperation(value = "Get List of Relation Infos (findInfoByTo)",
notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION)
@ApiOperation(value = "Get List of Relations (findEntityRelationsByTo)",
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')")
@GetMapping(value = "/relations/to/{toType}/{toId}")
public List<EntityRelation> findEntityRelationsByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByTo(strToId, strToType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@ -267,9 +302,19 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findInfoByTo(getTenantId(), entityId, typeGroup).get()));
}
@ApiOperation(value = "Get List of Relations (findByTo)",
notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " +
SECURITY_CHECKS_ENTITY_DESCRIPTION)
@ApiOperation(value = "Get List of Relation Infos (findEntityRelationInfosByTo)",
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')")
@GetMapping(value = "/relations/info/to/{toType}/{toId}")
public List<EntityRelationInfo> findEntityRelationInfosByTo(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException, ExecutionException, InterruptedException {
return findInfoByTo(strToId, strToType, strRelationTypeGroup);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@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,
@ -286,28 +331,41 @@ public class EntityRelationController extends BaseController {
return checkNotNull(filterRelationsByReadPermission(relationService.findByToAndType(getTenantId(), entityId, strRelationType, typeGroup)));
}
@ApiOperation(value = "Find related entities (findByQuery)",
@ApiOperation(value = "Get List of Relations (findEntityRelationsByToAndRelationType)",
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')")
@GetMapping(value = "/relations/to/{toType}/{toId}/{relationType}")
public List<EntityRelation> findEntityRelationsByToAndRelationType(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(TO_TYPE) String strToType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable(TO_ID) String strToId,
@Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @PathVariable(RELATION_TYPE) String strRelationType,
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION)
@RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
return findByTo(strToId, strToType, strRelationType, strRelationTypeGroup);
}
@ApiOperation(value = "Find related entities (findEntityRelationsByQuery)",
notes = "Returns all entities that are related to the specific entity. " +
"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')")
@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 {
public List<EntityRelation> findEntityRelationsByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query.getParameters());
checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ);
return checkNotNull(filterRelationsByReadPermission(relationService.findByQuery(getTenantId(), query).get()));
}
@ApiOperation(value = "Find related entity infos (findInfoByQuery)",
@ApiOperation(value = "Find related entity infos (findEntityRelationInfosByQuery)",
notes = "Returns all entity infos that are related to the specific entity. " +
"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')")
@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 {
public List<EntityRelationInfo> findEntityRelationInfosByQuery(@Parameter(description = "A JSON value representing the entity relations query object.", required = true)
@RequestBody EntityRelationsQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query.getParameters());
checkNotNull(query.getFilters());
checkEntityId(query.getParameters().getEntityId(), Operation.READ);

46
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -23,7 +24,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -79,7 +79,6 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -87,6 +86,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -167,17 +167,25 @@ public class EntityViewController extends BaseController {
tbEntityViewService.delete(entityView, getCurrentUser());
}
@ApiOperation(value = "Get Entity View by name (getTenantEntityView)",
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViews", params = {"entityViewName"})
public EntityView getTenantEntityView(
@Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(entityViewService.findEntityViewByTenantIdAndName(tenantId, entityViewName));
}
@ApiOperation(value = "Get Entity View by name (getTenantEntityViewByName)",
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityView")
public EntityView getTenantEntityViewByName(
@Parameter(description = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException {
return getTenantEntityView(entityViewName);
}
@ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)",
notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@ -222,7 +230,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/entityViews")
public PageData<EntityView> getCustomerEntityViews(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -254,7 +262,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/entityViewInfos")
public PageData<EntityViewInfo> getCustomerEntityViewInfos(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -286,7 +294,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViews", params = {"pageSize", "page"})
@GetMapping(value = "/tenant/entityViews")
public PageData<EntityView> getTenantEntityViews(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -314,7 +322,7 @@ public class EntityViewController extends BaseController {
notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"})
@GetMapping(value = "/tenant/entityViewInfos")
public PageData<EntityViewInfo> getTenantEntityViewInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -337,13 +345,13 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Find related entity views (findByQuery)",
@ApiOperation(value = "Find related entity views (findEntityViewsByQuery)",
notes = "Returns all entity views that are related to the specific entity. " +
"The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@PostMapping(value = "/entityViews")
public List<EntityView> findByQuery(
public List<EntityView> findEntityViewsByQuery(
@Parameter(description = "The entity view search query JSON")
@RequestBody EntityViewSearchQuery query) throws ThingsboardException, ExecutionException, InterruptedException {
checkNotNull(query);
@ -429,7 +437,7 @@ public class EntityViewController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/edge/{edgeId}/entityViews", params = {"pageSize", "page"})
@GetMapping(value = "/edge/{edgeId}/entityViews")
public PageData<EntityView> getEdgeEntityViews(
@PathVariable(EDGE_ID) String strEdgeId,
@RequestParam int pageSize,
@ -459,11 +467,10 @@ public class EntityViewController extends BaseController {
return checkNotNull(filteredResult);
}
@ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)",
notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ")
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/entityViews", params = {"entityViewIds"})
public List<EntityView> getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
public List<EntityView> getEntityViewsByIdsV1(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<EntityViewId> entityViewIds = new ArrayList<>();
@ -474,6 +481,15 @@ public class EntityViewController extends BaseController {
return filterEntityViewsByReadPermission(entityViews);
}
@ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)",
notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/entityViews/list")
public List<EntityView> getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException {
return getEntityViewsByIdsV1(entityViewUUIDs);
}
private List<EntityView> filterEntityViewsByReadPermission(List<EntityView> entityViews) {
return entityViews.stream().filter(entityView -> {
try {

19
application/src/main/java/org/thingsboard/server/controller/EventController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
@ -113,13 +114,13 @@ public class EventController extends BaseController {
@Autowired
private EventService eventService;
@ApiOperation(value = "Get Events by type (getEvents)",
@ApiOperation(value = "Get Events by type (getEventsByType)",
notes = "Returns a page of events for specified entity by specifying event type. " +
PAGE_DATA_PARAMETERS)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET)
@ResponseBody
public PageData<EventInfo> getEvents(
public PageData<EventInfo> getEventsByType(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -152,16 +153,12 @@ public class EventController extends BaseController {
return checkNotNull(eventService.findEvents(tenantId, entityId, resolveEventType(eventType), pageLink));
}
@ApiOperation(value = "Get Events (Deprecated)",
notes = "Returns a page of events for specified entity. Deprecated and will be removed in next minor release. " +
"The call was deprecated to improve the performance of the system. " +
"Current implementation will return 'Lifecycle' events only. " +
"Use 'Get events by type' or 'Get events by filter' instead. " +
PAGE_DATA_PARAMETERS)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<EventInfo> getEvents(
public PageData<EventInfo> getEventsDeprecated(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -194,14 +191,14 @@ public class EventController extends BaseController {
return checkNotNull(eventService.findEvents(tenantId, entityId, EventType.LC_EVENT, pageLink));
}
@ApiOperation(value = "Get Events by event filter (getEvents)",
@ApiOperation(value = "Get Events by event filter (getEventsByFilter)",
notes = "Returns a page of events for the chosen entity by specifying the event filter. " +
PAGE_DATA_PARAMETERS + NEW_LINE +
EVENT_FILTER_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST)
@ResponseBody
public PageData<EventInfo> getEvents(
public PageData<EventInfo> getEventsByFilter(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_TYPE) String strEntityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)

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

@ -69,11 +69,11 @@ public class Lwm2mController extends BaseController {
return lwM2MService.getServerSecurityInfo(bootstrapServer);
}
@ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)")
@ApiOperation(hidden = true, value = "Save LwM2M device with credentials (saveLwm2mDeviceWithCredentials) (Deprecated)")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST)
@ResponseBody
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
public Device saveLwm2mDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());

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

@ -43,12 +43,12 @@ public class MailConfigTemplateController extends BaseController {
private static final String MAIL_CONFIG_TEMPLATE_DEFINITION = "Mail configuration template is set of default smtp settings for mail server that specific provider supports";
private final TbMailConfigTemplateService mailConfigTemplateService;
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getMailConfigTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
notes = MAIL_CONFIG_TEMPLATE_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public JsonNode getClientRegistrationTemplates() throws ThingsboardException, IOException {
public JsonNode getMailConfigTemplates() throws ThingsboardException, IOException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return mailConfigTemplateService.findAllMailConfigTemplates();
}

6
application/src/main/java/org/thingsboard/server/controller/MobileAppBundleController.java

@ -81,12 +81,12 @@ public class MobileAppBundleController extends BaseController {
return tbMobileAppBundleService.save(mobileAppBundle, getOAuth2ClientIds(ids), getCurrentUser());
}
@ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
@ApiOperation(value = "Update oauth2 clients (updateMobileAppBundleOauth2Clients)",
notes = "Update oauth2 clients of the specified mobile app bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PutMapping(value = "/mobile/bundle/{id}/oauth2Clients")
public void updateOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
public void updateMobileAppBundleOauth2Clients(@PathVariable UUID id,
@RequestBody UUID[] clientIds) throws ThingsboardException {
MobileAppBundleId mobileAppBundleId = new MobileAppBundleId(id);
MobileAppBundle mobileAppBundle = checkMobileAppBundleId(mobileAppBundleId, Operation.WRITE);
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);

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

@ -123,7 +123,7 @@ public class MobileAppController extends BaseController {
return tbMobileAppService.save(mobileApp, getCurrentUser());
}
@ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Get mobile app infos (getTenantMobileApps)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app")
public PageData<MobileApp> getTenantMobileApps(@Parameter(description = "Platform type: ANDROID or IOS")
@ -142,7 +142,7 @@ public class MobileAppController extends BaseController {
return mobileAppService.findMobileAppsByTenantId(getTenantId(), platformType, pageLink);
}
@ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Get mobile info by id (getMobileAppById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/mobile/app/{id}")
public MobileApp getMobileAppById(@PathVariable UUID id) throws ThingsboardException {

41
application/src/main/java/org/thingsboard/server/controller/NotificationTargetController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -153,12 +154,10 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), (PlatformUsersNotificationTargetConfig) notificationTarget.getConfiguration(), pageLink);
}
@ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)",
notes = "Returns the list of notification targets found by provided ids." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@GetMapping(value = "/targets", params = {"ids"})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
public List<NotificationTarget> getNotificationTargetsByIdsV1(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) {
// PE: generic permission
@ -166,6 +165,17 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds);
}
@ApiOperation(value = "Get notification targets by ids (getNotificationTargetsByIds)",
notes = "Returns the list of notification targets found by provided ids." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets/list")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public List<NotificationTarget> getNotificationTargetsByIds(@Parameter(description = "Comma-separated list of uuids representing targets ids", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ids") UUID[] ids,
@AuthenticationPrincipal SecurityUser user) {
return getNotificationTargetsByIdsV1(ids, user);
}
@ApiOperation(value = "Get notification targets (getNotificationTargets)",
notes = "Returns the page of notification targets owned by sysadmin or tenant." + NEW_LINE +
PAGE_DATA_PARAMETERS +
@ -188,13 +198,10 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)",
notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@GetMapping(value = "/targets", params = "notificationType")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize,
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationTypeV1(@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@ -206,6 +213,22 @@ public class NotificationTargetController extends BaseController {
return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink);
}
@ApiOperation(value = "Get notification targets by supported notification type (getNotificationTargetsBySupportedNotificationType)",
notes = "Returns the page of notification targets filtered by notification type that they can be used for." + NEW_LINE +
PAGE_DATA_PARAMETERS +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping(value = "/targets/notificationType/{notificationType}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@PathVariable NotificationType notificationType,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
return getNotificationTargetsBySupportedNotificationTypeV1(pageSize, page, textSearch, sortProperty, sortOrder, notificationType, user);
}
@ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)",
notes = "Deletes notification target by its id." + NEW_LINE +
"This target cannot be referenced by existing scheduled notification requests or any notification rules." +

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

@ -71,12 +71,12 @@ public class OAuth2ConfigTemplateController extends BaseController {
oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId);
}
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get the list of all OAuth2 client registration templates (getOAuth2ClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() throws ThingsboardException {
public List<OAuth2ClientRegistrationTemplate> getOAuth2ClientRegistrationTemplates() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ);
return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates();
}

37
application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -115,32 +116,40 @@ public class OAuth2Controller extends BaseController {
return tbOauth2ClientService.save(oAuth2Client, getCurrentUser());
}
@ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Get OAuth2 Client infos (findOAuth2ClientInfos)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos")
public PageData<OAuth2ClientInfo> findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on client's title")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<OAuth2ClientInfo> findOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Case-insensitive 'substring' filter based on client's title")
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION)
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos", params = {"clientIds"})
public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIdsV1(
@RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException {
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
}
@ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)",
notes = "Fetch OAuth2 Client info objects based on the provided ids. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/oauth2/client/infos", params = {"clientIds"})
@GetMapping(value = "/oauth2/client/list")
public List<OAuth2ClientInfo> findTenantOAuth2ClientInfosByIds(
@Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException {
List<OAuth2ClientId> oAuth2ClientIds = getOAuth2ClientIds(clientIds);
return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
return findTenantOAuth2ClientInfosByIdsV1(clientIds);
}
@ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)

30
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -181,25 +181,25 @@ public class OtaPackageController extends BaseController {
return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink));
}
@ApiOperation(value = "Get OTA Package Infos (getOtaPackages)",
@ApiOperation(value = "Get OTA Package Infos by device profile and type (getOtaPackagesByDeviceProfileAndType)",
notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@PathVariable("type") String strType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
public PageData<OtaPackageInfo> getOtaPackagesByDeviceProfileAndType(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@PathVariable("type") String strType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "type", "title", "version", "tag", "url", "fileName", "dataSize", "checksum"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("deviceProfileId", strDeviceProfileId);
checkParameter("type", strType);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);

2
application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java

@ -129,7 +129,7 @@ public class QrCodeSettingsController extends BaseController {
return qrCodeSettingService.saveQrCodeSettings(currentUser.getTenantId(), qrCodeSettings);
}
@ApiOperation(value = "Get Mobile application settings (getMobileAppSettings)",
@ApiOperation(value = "Get Mobile application settings (getQrCodeSettings)",
notes = "The response payload contains configuration for android/iOS applications and platform qr code widget settings." + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/api/mobile/qr/settings")

8
application/src/main/java/org/thingsboard/server/controller/QueueController.java

@ -19,7 +19,9 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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;
@ -65,8 +67,7 @@ public class QueueController extends BaseController {
notes = "Returns a page of queues registered in the platform. " +
PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/queues")
public PageData<Queue> getTenantQueuesByServiceType(@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam String serviceType,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@ -122,8 +123,7 @@ public class QueueController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Queue entity. " +
SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/queues", params = {"serviceType"}, method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/queues")
public Queue saveQueue(@Parameter(description = "A JSON value representing the queue.")
@RequestBody Queue queue,
@Parameter(description = QUEUE_SERVICE_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"TB-RULE-ENGINE", "TB-CORE", "TB-TRANSPORT", "JS-EXECUTOR"}, requiredMode = Schema.RequiredMode.REQUIRED))

17
application/src/main/java/org/thingsboard/server/controller/QueueStatsController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -85,12 +86,10 @@ public class QueueStatsController extends BaseController {
return checkNotNull(queueStatsService.findQueueStatsById(getTenantId(), queueStatsId));
}
@ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)",
notes = "Fetch the Queue stats objects based on the provided ids. ")
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats", params = {"queueStatsIds"})
public List<QueueStats> getQueueStatsByIds(
@Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
public List<QueueStats> getQueueStatsByIdsV1(
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
checkArrayParameter("queueStatsIds", strQueueStatsIds);
List<QueueStatsId> queueStatsIds = new ArrayList<>();
@ -99,4 +98,14 @@ public class QueueStatsController extends BaseController {
}
return queueStatsService.findQueueStatsByIds(getTenantId(), queueStatsIds);
}
@ApiOperation(value = "Get QueueStats By Ids (getQueueStatsByIds)",
notes = "Fetch the Queue stats objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/queueStats/list")
public List<QueueStats> getQueueStatsByIds(
@Parameter(description = "A list of queue stats ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("queueStatsIds") String[] strQueueStatsIds) throws ThingsboardException {
return getQueueStatsByIdsV1(strQueueStatsIds);
}
}

16
application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -43,26 +45,28 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU
@Slf4j
public class RpcV1Controller extends AbstractRpcController {
@ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}
@ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV1)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}

15
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ -113,7 +114,7 @@ public class RpcV2Controller extends AbstractRpcController {
private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION)
@ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequestV2)", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."),
@ApiResponse(responseCode = "400", description = "Invalid structure of the request."),
@ -124,15 +125,16 @@ public class RpcV2Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequest(
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}
@ApiOperation(value = "Send two-way RPC request", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION)
@ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequestV2)", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Persistent RPC request was saved to the database or lightweight RPC response received."),
@ApiResponse(responseCode = "400", description = "Invalid structure of the request."),
@ -143,10 +145,11 @@ public class RpcV2Controller extends AbstractRpcController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequest(
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}

32
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -229,12 +230,12 @@ public class RuleChainController extends BaseController {
return tbRuleChainService.save(ruleChain, getCurrentUser());
}
@ApiOperation(value = "Create Default Rule Chain",
@ApiOperation(value = "Create Default Rule Chain (setDeviceDefaultRuleChain)",
notes = "Create rule chain from template, based on the specified name in the request. " +
"Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/ruleChain/device/default")
public RuleChain saveRuleChain(
public RuleChain setDeviceDefaultRuleChain(
@Parameter(description = "A JSON value representing the request.")
@RequestBody DefaultRuleChainCreateRequest request) throws Exception {
checkNotNull(request);
@ -281,7 +282,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chains (getRuleChains)",
notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains", params = {"pageSize", "page"})
@GetMapping(value = "/ruleChains")
public PageData<RuleChain> getRuleChains(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -347,7 +348,7 @@ public class RuleChainController extends BaseController {
notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/ruleChain/testScript")
public JsonNode testScript(
public JsonNode testRuleChainScript(
@Parameter(description = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.")
@ -409,7 +410,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains/export", params = {"limit"})
@GetMapping(value = "/ruleChains/export")
public RuleChainData exportRuleChains(
@Parameter(description = "A limit of rule chains to export.", required = true)
@RequestParam("limit") int limit) throws ThingsboardException {
@ -505,7 +506,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)",
notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"})
@GetMapping(value = "/edge/{edgeId}/ruleChains")
public PageData<RuleChain> getEdgeRuleChains(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId,
@ -582,14 +583,10 @@ public class RuleChainController extends BaseController {
return checkNotNull(result);
}
@ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)",
notes = "Requested rule chains must be owned by tenant which is performing the request. " +
NEW_LINE)
@Hidden
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains", params = {"ruleChainIds"})
public List<RuleChain> getRuleChainsByIds(
@Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
public List<RuleChain> getRuleChainsByIdsV1(@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainId> ruleChainIds = new ArrayList<>();
for (UUID ruleChainUUID : ruleChainUUIDs) {
@ -598,4 +595,15 @@ public class RuleChainController extends BaseController {
return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds);
}
@ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)",
notes = "Requested rule chains must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains/list")
public List<RuleChain> getRuleChainsByIds(
@Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
return getRuleChainsByIdsV1(ruleChainUUIDs);
}
}

40
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -17,6 +17,8 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -77,7 +79,7 @@ public class RuleEngineController extends BaseController {
@Autowired
private AccessValidator accessValidator;
@ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequest)",
@ApiOperation(value = "Push user message to the rule engine (handleRuleEngineRequestForUser)",
notes = MSG_DESCRIPTION_PREFIX +
"Uses current User Id ( the one which credentials is used to perform the request) as the Rule Engine message originator. " +
MSG_DESCRIPTION +
@ -86,13 +88,14 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest(
@Parameter(description = "A JSON value representing the message.", required = true)
public DeferredResult<ResponseEntity> handleRuleEngineRequestForUser(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody);
return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody);
}
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)",
@ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequestForEntity)",
notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION +
@ -101,17 +104,18 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest(
public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, defaultResponseTimeout, requestBody);
return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody);
}
@ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)",
@ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequestForEntityWithTimeout)",
notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION +
@ -120,19 +124,20 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{timeout}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest(
public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntityWithTimeout(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = "Timeout to process the request in milliseconds", required = true)
@PathVariable("timeout") int timeout,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequest(entityType, entityIdStr, null, timeout, requestBody);
return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody);
}
@ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequest)",
@ApiOperation(value = "Push entity message with timeout and specified queue to the rule engine (handleRuleEngineRequestForEntityWithQueueAndTimeout)",
notes = MSG_DESCRIPTION_PREFIX +
"Uses specified Entity Id as the Rule Engine message originator. " +
MSG_DESCRIPTION +
@ -142,7 +147,7 @@ public class RuleEngineController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{queueName}/{timeout}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequest(
public DeferredResult<ResponseEntity> handleRuleEngineRequestForEntityWithQueueAndTimeout(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@ -151,7 +156,8 @@ public class RuleEngineController extends BaseController {
@PathVariable("queueName") String queueName,
@Parameter(description = "Timeout to process the request in milliseconds", required = true)
@PathVariable("timeout") int timeout,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
try {
SecurityUser currentUser = getCurrentUser();
@ -244,5 +250,7 @@ public class RuleEngineController extends BaseController {
response != null ? response.getData() : "");
}
private record LocalRequestMetaData(TbMsg request, SecurityUser user, DeferredResult<ResponseEntity> responseWriter) {}
private record LocalRequestMetaData(TbMsg request, SecurityUser user,
DeferredResult<ResponseEntity> responseWriter) {
}
}

19
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -118,7 +119,7 @@ public class TbResourceController extends BaseController {
.body(resource);
}
@ApiOperation(value = "Download resource (downloadResource)",
@ApiOperation(value = "Download resource (downloadResourceIfChanged)",
notes = "Download resource with a given type and key for the given scope" + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/resource/{resourceType}/{scope}/{key}")
@ -336,11 +337,10 @@ public class TbResourceController extends BaseController {
}
}
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource", params = {"resourceIds"})
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
public List<TbResourceInfo> getSystemOrTenantResourcesByIdsV1(
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
SecurityUser user = getCurrentUser();
List<TbResourceId> resourceIds = new ArrayList<>();
@ -350,7 +350,16 @@ public class TbResourceController extends BaseController {
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
}
@ApiOperation(value = "Get All Resource Infos (getAllResources)",
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource/list")
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
return getSystemOrTenantResourcesByIdsV1(resourceUuids);
}
@ApiOperation(value = "Get All Resource Infos (getTenantResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")

110
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -24,7 +24,12 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ -166,7 +171,8 @@ public class TelemetryController extends BaseController {
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes")
public DeferredResult<ResponseEntity> getAttributeKeys(
@ -180,7 +186,8 @@ public class TelemetryController extends BaseController {
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributeKeysByScope(
@ -197,13 +204,18 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = AttributeData.class)))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/attributes")
public DeferredResult<ResponseEntity> getAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
@ -220,7 +232,11 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = AttributeData.class)))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}")
public DeferredResult<ResponseEntity> getAttributesByScope(
@ -228,6 +244,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
@ -237,7 +254,8 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
notes = "Returns a set of unique time series key names for the selected entity. " +
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(type = "string")))))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/keys/timeseries")
public DeferredResult<ResponseEntity> getTimeseriesKeys(
@ -258,7 +276,11 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START
+ LATEST_TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries")
public DeferredResult<ResponseEntity> getLatestTimeseries(
@ -267,6 +289,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
@ -274,7 +297,32 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes));
}
@ApiOperation(value = "Get time series data (getTimeseries)",
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"})
public DeferredResult<ResponseEntity> getTimeseries(
@PathVariable("entityType") String entityType,
@PathVariable("entityId") String entityIdStr,
@RequestParam(name = "keys", required = false) String keysStr,
@RequestParam(name = "startTs") Long startTs,
@RequestParam(name = "endTs") Long endTs,
@RequestParam(name = "intervalType", required = false) IntervalType intervalType,
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@RequestParam(name = "timeZone", required = false) String timeZone,
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
}
@ApiOperation(value = "Get time series data (getTimeseriesHistory)",
notes = "Returns a range of time series values for specified entity. " +
"Returns not aggregated data by default. " +
"Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " +
@ -282,10 +330,14 @@ public class TelemetryController extends BaseController {
+ MARKDOWN_CODE_BLOCK_START
+ TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
responses = @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(type = "object", additionalPropertiesSchema = TsData[].class))))
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"})
public DeferredResult<ResponseEntity> getTimeseries(
@GetMapping(value = "/{entityType}/{entityId}/values/timeseries/history")
public DeferredResult<ResponseEntity> getTimeseriesHistory(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@ -310,13 +362,9 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
return getTimeseries(entityType, entityIdStr, keysStr, startTs, endTs, intervalType, interval, timeZone, limit, aggStr, orderBy, useStrictDataTypes, params);
}
@ApiOperation(value = "Save device attributes (saveDeviceAttributes)",
@ -338,7 +386,8 @@ public class TelemetryController extends BaseController {
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -363,7 +412,8 @@ public class TelemetryController extends BaseController {
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -388,7 +438,8 @@ public class TelemetryController extends BaseController {
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -412,7 +463,8 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, 0L);
}
@ -436,7 +488,8 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope,
@Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, ttl);
}
@ -449,6 +502,9 @@ public class TelemetryController extends BaseController {
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." +
" The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ -473,6 +529,7 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
@Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
@ -530,6 +587,9 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)",
notes = "Delete device attributes using provided Device Id, scope and a list of keys. " +
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ -544,6 +604,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
@ -552,9 +613,13 @@ public class TelemetryController extends BaseController {
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " +
"This operation is idempotent: keys that do not exist are silently ignored and the response is still 200 OK. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Parameters({
@Parameter(name = "key", description = "Repeatable key query parameter (alternative to comma-separated 'keys')", in = ParameterIn.QUERY, required = false, array = @ArraySchema(schema = @Schema(type = "string")))
})
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " +
@ApiResponse(responseCode = "200", description = "Entity attributes were removed for the selected keys in the request (keys that did not exist are silently ignored). " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ -568,6 +633,7 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(hidden = true)
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);

18
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -22,7 +23,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -133,7 +133,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenants", params = {"pageSize", "page"})
@GetMapping(value = "/tenants")
public PageData<Tenant> getTenants(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -152,7 +152,7 @@ public class TenantController extends BaseController {
@ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. "
+ TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenantInfos", params = {"pageSize", "page"})
@GetMapping(value = "/tenantInfos")
public PageData<TenantInfo> getTenantInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -169,9 +169,10 @@ public class TenantController extends BaseController {
return checkNotNull(tenantService.findTenantInfos(pageLink));
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/tenants", params = {"tenantIds"})
public List<Tenant> getTenantsByIds(
public List<Tenant> getTenantsByIdsV1(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
@ -183,6 +184,15 @@ public class TenantController extends BaseController {
return filterTenantsByReadPermission(tenants);
}
@ApiOperation(value = "Get Tenants list (getTenantsByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/tenants/list")
public List<Tenant> getTenantsByIds(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException {
return getTenantsByIdsV1(tenantUUIDs);
}
private List<Tenant> filterTenantsByReadPermission(List<Tenant> tenants) {
return tenants.stream().filter(tenant -> {
try {

21
application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -191,7 +192,7 @@ public class TenantProfileController extends BaseController {
oldProfile = checkTenantProfileId(tenantProfile.getId(), Operation.WRITE);
}
return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile);
return tbTenantProfileService.save(getTenantId(), tenantProfile, oldProfile, getCurrentUser());
}
@ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)",
@ -204,7 +205,7 @@ public class TenantProfileController extends BaseController {
checkParameter("tenantProfileId", strTenantProfileId);
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE);
tbTenantProfileService.delete(getTenantId(), profile);
tbTenantProfileService.delete(getTenantId(), profile, getCurrentUser());
}
@ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)",
@ -217,7 +218,7 @@ public class TenantProfileController extends BaseController {
checkParameter("tenantProfileId", strTenantProfileId);
TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE);
tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId);
tenantProfile = tbTenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfile, getCurrentUser());
return tenantProfile;
}
@ -242,7 +243,7 @@ public class TenantProfileController extends BaseController {
@ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. "
+ TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"})
@GetMapping(value = "/tenantProfileInfos")
public PageData<EntityInfo> getTenantProfileInfos(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -258,11 +259,19 @@ public class TenantProfileController extends BaseController {
return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink));
}
@Hidden
@GetMapping(value = "/tenantProfiles", params = {"ids"})
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfilesByIds(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("ids") UUID[] ids) {
public List<TenantProfile> getTenantProfilesByIds(@RequestParam("ids") UUID[] ids) {
return tenantProfileService.findTenantProfilesByIds(TenantId.SYS_TENANT_ID, ids);
}
@ApiOperation(value = "Get Tenant Profile list (getTenantProfileList)")
@GetMapping(value = "/tenantProfiles/list")
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public List<TenantProfile> getTenantProfileList(@Parameter(description = "Comma-separated list of tenant profile ids", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("ids") UUID[] ids) {
return getTenantProfilesByIds(ids);
}
}

9
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -16,7 +16,11 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@ -187,7 +191,7 @@ public class TwoFactorAuthConfigController extends BaseController {
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType);
}
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderTypes)", notes =
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
"Example of response:\n" +
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" +
@ -195,7 +199,7 @@ public class TwoFactorAuthConfigController extends BaseController {
)
@GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
public List<TwoFaProviderType> getAvailableTwoFaProviderTypes() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
.map(TwoFaProviderConfig::getProviderType)
@ -261,6 +265,7 @@ public class TwoFactorAuthConfigController extends BaseController {
}
@Data
@Schema
public static class TwoFaAccountConfigUpdateRequest {
private boolean useByDefault;
}

6
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -99,7 +100,7 @@ public class TwoFactorAuthController extends BaseController {
}
}
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviderInfos)", notes =
"Get the list of 2FA provider infos available for user to use. Example:\n" +
"```\n[\n" +
" {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" +
@ -108,7 +109,7 @@ public class TwoFactorAuthController extends BaseController {
"]\n```")
@GetMapping("/providers")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
public List<TwoFaProviderInfo> getAvailableTwoFaProviderInfos() throws ThingsboardException {
SecurityUser user = getCurrentUser();
Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true);
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user)
@ -166,6 +167,7 @@ public class TwoFactorAuthController extends BaseController {
@Builder
public static class TwoFaProviderInfo {
private TwoFaProviderType type;
@JsonProperty("default")
private boolean isDefault;
private String contact;
private Integer minVerificationCodeSendPeriod;

80
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -273,7 +274,7 @@ public class UserController extends BaseController {
notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users", params = {"pageSize", "page"})
@GetMapping(value = "/users")
public PageData<User> getUsers(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ -334,7 +335,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Tenant Users (getTenantAdmins)",
notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"})
@GetMapping(value = "/tenant/{tenantId}/users")
public PageData<User> getTenantAdmins(
@Parameter(description = TENANT_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(TENANT_ID) String strTenantId,
@ -357,7 +358,7 @@ public class UserController extends BaseController {
@ApiOperation(value = "Get Customer Users (getCustomerUsers)",
notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"})
@GetMapping(value = "/customer/{customerId}/users")
public PageData<User> getCustomerUsers(
@Parameter(description = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ -404,7 +405,7 @@ public class UserController extends BaseController {
"Search is been executed by email, firstName and lastName fields. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users/assign/{alarmId}", params = {"pageSize", "page"})
@GetMapping(value = "/users/assign/{alarmId}")
public PageData<UserEmailInfo> getUsersForAssign(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("alarmId") String strAlarmId,
@ -456,10 +457,7 @@ public class UserController extends BaseController {
return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings();
}
@ApiOperation(value = "Update user settings (saveUserSettings)",
notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(value = "/user/settings")
public void putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
@ -467,8 +465,17 @@ public class UserController extends BaseController {
userSettingsService.updateUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, settings);
}
@ApiOperation(value = "Get user settings (getUserSettings)",
notes = "Fetch the User settings based on authorized user. ")
@ApiOperation(value = "Update user settings (putGeneralUserSettings)",
notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@PutMapping(value = "/user/settings/general")
public void putGeneralUserSettings(@RequestBody JsonNode settings) throws ThingsboardException {
putUserSettings(settings);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/settings")
public JsonNode getUserSettings() throws ThingsboardException {
@ -478,20 +485,28 @@ public class UserController extends BaseController {
return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings();
}
@ApiOperation(value = "Delete user settings (deleteUserSettings)",
@ApiOperation(value = "Get user settings (getGeneralUserSettings)",
notes = "Fetch the User settings based on authorized user. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/settings/general")
public JsonNode getGeneralUserSettings() throws ThingsboardException {
return getUserSettings();
}
@ApiOperation(value = "Delete user settings (deleteGeneralUserSettings)",
notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/user/settings/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException {
public void deleteGeneralUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths) throws ThingsboardException {
checkParameter(USER_ID, paths);
SecurityUser currentUser = getCurrentUser();
userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), UserSettingsType.GENERAL, Arrays.asList(paths.split(",")));
}
@ApiOperation(value = "Update user settings (saveUserSettings)",
@ApiOperation(value = "Update user settings (putUserSettings)",
notes = "Update user settings for authorized user. Only specified json elements will be updated." +
"Example: you have such settings: {A:5, B:{C:10, D:20}}. Updating it with {B:{C:10, D:30}} will result in" +
"{A:5, B:{C:10, D:30}}. The same could be achieved by putting {B.D:30}")
@ -518,15 +533,15 @@ public class UserController extends BaseController {
return userSettings == null ? JacksonUtil.newObjectNode() : userSettings.getSettings();
}
@ApiOperation(value = "Delete user settings (deleteUserSettings)",
@ApiOperation(value = "Delete user settings by type (deleteUserSettingsByType)",
notes = "Delete user settings by specifying list of json element xpaths. \n " +
"Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@DeleteMapping(value = "/user/settings/{type}/{paths}")
public void deleteUserSettings(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths,
@Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".")
@PathVariable("type") String strType) throws ThingsboardException {
public void deleteUserSettingsByType(@Parameter(description = PATHS)
@PathVariable(PATHS) String paths,
@Parameter(description = "Settings type, case insensitive, one of: \"general\", \"quick_links\", \"doc_links\" or \"dashboards\".")
@PathVariable("type") String strType) throws ThingsboardException {
checkParameter(USER_ID, paths);
UserSettingsType type = checkEnumParameter("Settings type", strType, UserSettingsType::valueOf);
checkNotReserved(strType, type);
@ -534,8 +549,7 @@ public class UserController extends BaseController {
userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), type, Arrays.asList(paths.split(",")));
}
@ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)",
notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/dashboards")
public UserDashboardsInfo getUserDashboardsInfo() throws ThingsboardException {
@ -543,6 +557,14 @@ public class UserController extends BaseController {
return userSettingsService.findUserDashboardsInfo(currentUser.getTenantId(), currentUser.getId());
}
@ApiOperation(value = "Get information about last visited and starred dashboards (getLastVisitedDashboards)",
notes = "Fetch the list of last visited and starred dashboards. Both lists are limited to 10 items." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/user/lastVisitedDashboards")
public UserDashboardsInfo getLastVisitedDashboards() throws ThingsboardException {
return getUserDashboardsInfo();
}
@ApiOperation(value = "Report action of User over the dashboard (reportUserDashboardAction)",
notes = "Report action of User over the dashboard. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@ -583,12 +605,10 @@ public class UserController extends BaseController {
userService.removeMobileSession(user.getTenantId(), mobileToken);
}
@ApiOperation(value = "Get Users By Ids (getUsersByIds)",
notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ")
@Hidden
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users", params = {"userIds"})
public List<User> getUsersByIds(
@Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
public List<User> getUsersByIdsV1(
@RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<UserId> userIds = new ArrayList<>();
@ -599,6 +619,16 @@ public class UserController extends BaseController {
return filterUsersByReadPermission(users);
}
@ApiOperation(value = "Get Users By Ids (getUsersByIds)",
notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users/list")
public List<User> getUsersByIds(
@Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException {
return getUsersByIdsV1(userUUIDs);
}
private List<User> filterUsersByReadPermission(List<User> users) {
return users.stream().filter(user -> {
try {

30
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -209,8 +210,7 @@ public class WidgetTypeController extends AutoCommitController {
}
}
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypesByBundleAlias) (Deprecated)",
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypes", params = {"isSystem", "bundleAlias"})
@Deprecated
@ -229,19 +229,27 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundle.getId()));
}
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)",
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetTypes", params = {"widgetsBundleId"})
public List<WidgetType> getBundleWidgetTypes(
public List<WidgetType> getBundleWidgetTypesV1(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesByWidgetsBundleId(getTenantId(), widgetsBundleId));
}
@ApiOperation(value = "Get all Widget types details for specified Bundle (getBundleWidgetTypesDetailsByBundleAlias) (Deprecated)",
notes = "Returns an array of Widget Type Details objects that belong to specified Widget Bundle." + WIDGET_TYPE_DETAILS_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Get all Widget types for specified Bundle (getBundleWidgetTypes)",
notes = "Returns an array of Widget Type objects that belong to specified Widget Bundle." + WIDGET_TYPE_DESCRIPTION + " " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypes")
public List<WidgetType> getBundleWidgetTypes(
@Parameter(description = "Widget Bundle Id", required = true)
@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
return getBundleWidgetTypesV1(strWidgetsBundleId);
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"})
@Deprecated
@ -284,7 +292,7 @@ public class WidgetTypeController extends AutoCommitController {
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",
notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"})
@GetMapping(value = "/widgetTypeFqns")
public List<String> getBundleWidgetTypeFqns(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@ -292,8 +300,7 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId));
}
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"})
@Deprecated
@ -344,8 +351,7 @@ public class WidgetTypeController extends AutoCommitController {
widgetTypeDeprecatedFilter, widgetTypes, pageLink));
}
@ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)",
notes = "Get the Widget Type based on the provided parameters. " + WIDGET_TYPE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"})
@Deprecated

31
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -15,13 +15,13 @@
*/
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -222,11 +222,10 @@ public class WidgetsBundleController extends BaseController {
}
}
@ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)",
notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles")
public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
public List<WidgetsBundle> getWidgetsBundlesV1() throws ThingsboardException {
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetsBundleService.findSystemWidgetsBundles(getTenantId()));
} else {
@ -235,13 +234,18 @@ public class WidgetsBundleController extends BaseController {
}
}
@ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesByIds)",
notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " +
NEW_LINE)
@ApiOperation(value = "Get all Widget Bundles (getAllWidgetsBundles)",
notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles/all")
public List<WidgetsBundle> getAllWidgetsBundles() throws ThingsboardException {
return getWidgetsBundlesV1();
}
@Hidden
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"})
public List<WidgetsBundle> getWidgetsBundlesByIds(
@Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException {
List<WidgetsBundleId> widgetsBundleIds = new ArrayList<>();
for (UUID widgetsBundleUUID : widgetsBundleUUIDs) {
@ -250,4 +254,15 @@ public class WidgetsBundleController extends BaseController {
return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds);
}
@ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesList)",
notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles/list", params = {"widgetsBundleIds"})
public List<WidgetsBundle> getWidgetsBundlesList(
@Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException {
return getWidgetsBundlesByIds(widgetsBundleUUIDs);
}
}

6
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -115,7 +115,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private final ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private Cache<String, SessionMetaData> pendingSessions;
@ -611,7 +611,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
&& UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser();
if (limitAllowed) {
@ -655,7 +655,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.remove(sessionId);
}

3
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.action;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -235,7 +236,7 @@ public class EntityActionService {
}
}
public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
public <E extends HasName, I extends EntityId> void logEntityAction(User user, @NotNull I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId();

8
application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.ai;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.TextContent;
@ -42,7 +43,12 @@ class AiChatModelServiceImpl implements AiChatModelService {
@Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
ChatModel langChainChatModel;
try {
langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
} catch (Throwable t) {
return FluentFuture.from(Futures.immediateFailedFuture(t));
}
if (langChainChatModel.provider() == ModelProvider.GITHUB_MODELS) {
chatRequest = prepareGithubChatRequest(chatRequest);
}

13
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -37,6 +37,7 @@ import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.SsrfProtectionValidator;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
@ -58,6 +59,7 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
@ -69,6 +71,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
validateBaseUrl(chatModelConfig.providerConfig().baseUrl());
return OpenAiChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.apiKey(chatModelConfig.providerConfig().apiKey())
@ -86,6 +89,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
validateBaseUrl(providerConfig.endpoint());
return AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion())
@ -177,8 +181,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
if (chatModelConfig.presencePenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.presencePenalty().floatValue());
}
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
@ -273,6 +277,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
validateBaseUrl(chatModelConfig.providerConfig().baseUrl());
var builder = OllamaChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.modelName(chatModelConfig.modelId())
@ -300,6 +305,10 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
return builder.build();
}
private static void validateBaseUrl(String url) {
SsrfProtectionValidator.validateUri(URI.create(url));
}
private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
}

2
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java

@ -61,7 +61,7 @@ public interface CalculatedFieldCache {
void addOwnerEntity(TenantId tenantId, EntityId entityId);
void evictEntity(EntityId entityId);
void evictOwnerEntity(EntityId entityId);
void evictOwner(EntityId owner);

126
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.server.actors.ActorSystemContext;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.AfterStartUp;
@ -45,7 +48,9 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -53,6 +58,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@ -268,18 +274,25 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public void updateOwnerEntity(TenantId tenantId, EntityId entityId) {
evictEntity(entityId);
evictOwnerEntity(entityId);
addOwnerEntity(tenantId, entityId);
}
@Override
public void evictEntity(EntityId entityId) {
public void evictOwnerEntity(EntityId entityId) {
ownerEntities.values().forEach(entities -> entities.remove(entityId));
}
@Override
public void evictOwner(EntityId owner) {
ownerEntities.remove(owner);
Set<EntityId> removedEntities = ownerEntities.remove(owner);
if (removedEntities != null) {
Set<EntityId> removedCustomers = removedEntities
.stream()
.filter(entityId -> entityId.getEntityType() == EntityType.CUSTOMER)
.collect(Collectors.toSet());
removedCustomers.forEach(this::evictOwner);
}
}
private Set<EntityId> getOwnedEntities(TenantId tenantId, EntityId ownerId) {
@ -290,6 +303,113 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
});
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
TenantProfileId tenantProfileId = new TenantProfileId(event.getEntityId().getId());
handleTenantProfileUpdate(tenantProfileId);
}
break;
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
evictTenantCfs(tenantId);
evictOwner(tenantId);
}
break;
case CUSTOMER:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwner(event.getEntityId());
evictOwnerEntity(event.getEntityId());
}
break;
case DEVICE, ASSET:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwnerEntity(event.getEntityId());
evictEntityCfs(event.getEntityId());
}
break;
case DEVICE_PROFILE, ASSET_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictEntityCfs(event.getEntityId());
}
break;
case CALCULATED_FIELD:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else {
evict((CalculatedFieldId) event.getEntityId());
}
break;
}
}
private void evictTenantCfs(TenantId tenantId) {
var removedCfEntityIds = new HashSet<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
var toRemove = calculatedFields.entrySet().stream()
.filter(e -> e.getValue().getTenantId().equals(tenantId))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
toRemove.forEach(cfId -> {
CalculatedField cf = calculatedFields.remove(cfId);
List<CalculatedFieldLink> links = calculatedFieldLinks.remove(cfId);
if (links != null) {
links.forEach(link -> removedLinkEntityIds.add(link.entityId()));
}
calculatedFieldsCtx.remove(cfId);
if (cf != null) {
removedCfEntityIds.add(cf.getEntityId());
}
});
removedCfEntityIds.forEach(entityId -> {
entityIdCalculatedFields.compute(entityId, (k, cfs) -> {
if (cfs != null) {
cfs.removeIf(cf -> toRemove.contains(cf.getId()));
return cfs.isEmpty() ? null : cfs;
}
return null;
});
});
removedLinkEntityIds.forEach(entityId -> {
entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
if (links != null) {
links.removeIf(link -> toRemove.contains(link.calculatedFieldId()));
return links.isEmpty() ? null : links;
}
return null;
}));
});
}
private void evictEntityCfs(EntityId entityId) {
List<CalculatedField> cfs = entityIdCalculatedFields.remove(entityId);
if (cfs != null) {
var cfIds = new HashSet<CalculatedFieldId>();
cfs.forEach(cf -> {
calculatedFields.remove(cf.getId());
calculatedFieldLinks.remove(cf.getId());
calculatedFieldsCtx.remove(cf.getId());
cfIds.add(cf.getId());
log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf);
});
entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.calculatedFieldId())));
}
entityIdCalculatedFieldLinks.remove(entityId);
}
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}

3
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -305,7 +305,8 @@ public class CalculatedFieldCtx implements Closeable {
public void setTenantProfileProperties() {
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
if (tenantProfile == null) {
throw new IllegalStateException("Tenant Profile not found for tenant: " + tenantId);
log.warn("[{}][{}][{}] Tenant Profile not found for tenant: {}. CF limits and thresholds will not be updated.", tenantId, entityId, cfId, tenantId);
return;
}
tenantProfile.getProfileConfiguration().ifPresent(config -> {
this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L;

1
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -179,6 +179,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
ruleState.setActive(null);
AlarmCondition condition = rule.getCondition();
if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) {
ruleState.cancelDurationCheckFuture();
reevalNeeded.set(true);
}
}

12
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -256,10 +256,7 @@ public class AlarmRuleState {
firstEventTs = 0L;
lastCheckTs = 0L;
duration = 0L;
if (durationCheckFuture != null) {
durationCheckFuture.cancel(true);
durationCheckFuture = null;
}
cancelDurationCheckFuture();
}
public void setDurationCheckFuture(ScheduledFuture<?> durationCheckFuture) {
@ -270,6 +267,13 @@ public class AlarmRuleState {
this.durationCheckFuture = durationCheckFuture;
}
public void cancelDurationCheckFuture() {
if (durationCheckFuture != null) {
durationCheckFuture.cancel(true);
durationCheckFuture = null;
}
}
public boolean isEmpty() {
return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null;
}

12
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -25,6 +25,7 @@ import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.pat.ApiKeyService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.apikey.ApiKeyProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -78,6 +80,7 @@ import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdge
import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor;
import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService;
import org.thingsboard.server.service.executors.GrpcCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.EnumMap;
import java.util.List;
@ -104,6 +107,9 @@ public class EdgeContextComponent {
}
// services
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired
private AdminSettingsService adminSettingsService;
@ -269,6 +275,12 @@ public class EdgeContextComponent {
@Autowired
private AiModelProcessor aiModelProcessor;
@Autowired
private ApiKeyService apiKeyService;
@Autowired
private ApiKeyProcessor apiKeyProcessor;
@Autowired
private UserProcessor userProcessor;

38
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset;
@ -60,6 +62,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -97,6 +100,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -193,6 +197,10 @@ public class EdgeMsgConstructorUtils {
)
);
private static void resetVersion(HasVersion entity) {
entity.setVersion(null);
}
public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) {
return AlarmUpdateMsg.newBuilder().setMsgType(msgType)
.setEntity(JacksonUtil.toString(alarm))
@ -205,6 +213,7 @@ public class EdgeMsgConstructorUtils {
}
public static AssetUpdateMsg constructAssetUpdatedMsg(UpdateMsgType msgType, Asset asset) {
resetVersion(asset);
return AssetUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(asset))
.setIdMSB(asset.getUuidId().getMostSignificantBits())
.setIdLSB(asset.getUuidId().getLeastSignificantBits()).build();
@ -218,6 +227,7 @@ public class EdgeMsgConstructorUtils {
}
public static AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) {
resetVersion(assetProfile);
return AssetProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(assetProfile))
.setIdMSB(assetProfile.getId().getId().getMostSignificantBits())
.setIdLSB(assetProfile.getId().getId().getLeastSignificantBits()).build();
@ -231,6 +241,7 @@ public class EdgeMsgConstructorUtils {
}
public static CustomerUpdateMsg constructCustomerUpdatedMsg(UpdateMsgType msgType, Customer customer) {
resetVersion(customer);
return CustomerUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(customer))
.setIdMSB(customer.getId().getId().getMostSignificantBits())
.setIdLSB(customer.getId().getId().getLeastSignificantBits()).build();
@ -244,6 +255,7 @@ public class EdgeMsgConstructorUtils {
}
public static DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) {
resetVersion(dashboard);
return DashboardUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(dashboard))
.setIdMSB(dashboard.getId().getId().getMostSignificantBits())
.setIdLSB(dashboard.getId().getId().getLeastSignificantBits()).build();
@ -257,6 +269,7 @@ public class EdgeMsgConstructorUtils {
}
public static DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device) {
resetVersion(device);
return DeviceUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(device))
.setIdMSB(device.getId().getId().getMostSignificantBits())
.setIdLSB(device.getId().getId().getLeastSignificantBits()).build();
@ -270,10 +283,12 @@ public class EdgeMsgConstructorUtils {
}
public static DeviceCredentialsUpdateMsg constructDeviceCredentialsUpdatedMsg(DeviceCredentials deviceCredentials) {
resetVersion(deviceCredentials);
return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build();
}
public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
resetVersion(deviceProfile);
String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion);
return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity)
.setIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
@ -387,6 +402,7 @@ public class EdgeMsgConstructorUtils {
}
public static EntityViewUpdateMsg constructEntityViewUpdatedMsg(UpdateMsgType msgType, EntityView entityView) {
resetVersion(entityView);
return EntityViewUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityView))
.setIdMSB(entityView.getId().getId().getMostSignificantBits())
.setIdLSB(entityView.getId().getId().getLeastSignificantBits()).build();
@ -486,6 +502,7 @@ public class EdgeMsgConstructorUtils {
}
public static RelationUpdateMsg constructRelationUpdatedMsg(UpdateMsgType msgType, EntityRelation entityRelation) {
resetVersion(entityRelation);
return RelationUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(entityRelation)).build();
}
@ -503,6 +520,7 @@ public class EdgeMsgConstructorUtils {
}
public static RuleChainUpdateMsg constructRuleChainUpdatedMsg(UpdateMsgType msgType, RuleChain ruleChain, boolean isRoot) {
resetVersion(ruleChain);
boolean isTemplateRoot = ruleChain.isRoot();
ruleChain.setRoot(isRoot);
RuleChainUpdateMsg result = RuleChainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChain))
@ -520,6 +538,7 @@ public class EdgeMsgConstructorUtils {
}
public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) {
resetVersion(ruleChainMetaData);
String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion);
return RuleChainMetadataUpdateMsg.newBuilder()
@ -640,6 +659,7 @@ public class EdgeMsgConstructorUtils {
}
public static TenantUpdateMsg constructTenantUpdateMsg(UpdateMsgType msgType, Tenant tenant) {
resetVersion(tenant);
return TenantUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(tenant)).build();
}
@ -648,6 +668,7 @@ public class EdgeMsgConstructorUtils {
}
public static UserUpdateMsg constructUserUpdatedMsg(UpdateMsgType msgType, User user) {
resetVersion(user);
return UserUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(user))
.setIdMSB(user.getId().getId().getMostSignificantBits())
.setIdLSB(user.getId().getId().getLeastSignificantBits()).build();
@ -665,6 +686,7 @@ public class EdgeMsgConstructorUtils {
}
public static WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) {
resetVersion(widgetsBundle);
return WidgetsBundleUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetsBundle))
.setWidgets(JacksonUtil.toString(widgets))
.setIdMSB(widgetsBundle.getId().getId().getMostSignificantBits())
@ -680,6 +702,7 @@ public class EdgeMsgConstructorUtils {
}
public static WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails) {
resetVersion(widgetTypeDetails);
return WidgetTypeUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(widgetTypeDetails))
.setIdMSB(widgetTypeDetails.getId().getId().getMostSignificantBits())
.setIdLSB(widgetTypeDetails.getId().getId().getLeastSignificantBits()).build();
@ -694,6 +717,7 @@ public class EdgeMsgConstructorUtils {
}
public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) {
resetVersion(calculatedField);
return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField))
.setIdMSB(calculatedField.getId().getId().getMostSignificantBits())
.setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build();
@ -707,6 +731,7 @@ public class EdgeMsgConstructorUtils {
}
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
resetVersion(aiModel);
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
.setIdMSB(aiModel.getId().getId().getMostSignificantBits())
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
@ -719,6 +744,19 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
public static ApiKeyUpdateMsg constructApiKeyUpdatedMsg(UpdateMsgType msgType, ApiKey apiKey) {
return ApiKeyUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(apiKey))
.setIdMSB(apiKey.getId().getId().getMostSignificantBits())
.setIdLSB(apiKey.getId().getId().getLeastSignificantBits()).build();
}
public static ApiKeyUpdateMsg constructApiKeyDeleteMsg(ApiKeyId apiKeyId) {
return ApiKeyUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(apiKeyId.getId().getMostSignificantBits())
.setIdLSB(apiKeyId.getId().getLeastSignificantBits()).build();
}
public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
try {
edgeEvents = removeDownlinkDuplicates(edgeEvents);

43
application/src/main/java/org/thingsboard/server/service/edge/rpc/AttributeSaveCallback.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc;
import com.google.common.util.concurrent.FutureCallback;
import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
@Slf4j
@AllArgsConstructor
public class AttributeSaveCallback implements FutureCallback<Void> {
private final TenantId tenantId;
private final EdgeId edgeId;
private final String key;
private final Object value;
@Override
public void onSuccess(@Nullable Void result) {
log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t);
}
}

37
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -140,9 +140,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Lazy
private EdgeContextComponent ctx;
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired
private TbClusterService clusterService;
@ -583,14 +580,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void save(TenantId tenantId, EdgeId edgeId, String key, long value) {
log.debug("[{}][{}] Updating long edge telemetry [{}] [{}]", tenantId, edgeId, key, value);
if (persistToTelemetry) {
tsSubService.saveTimeseries(TimeseriesSaveRequest.builder()
ctx.getTsSubService().saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edgeId)
.entry(new LongDataEntry(key, value))
.callback(new AttributeSaveCallback(tenantId, edgeId, key, value))
.build());
} else {
tsSubService.saveAttributes(AttributesSaveRequest.builder()
ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edgeId)
.scope(AttributeScope.SERVER_SCOPE)
@ -603,14 +600,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void save(TenantId tenantId, EdgeId edgeId, String key, boolean value) {
log.debug("[{}][{}] Updating boolean edge telemetry [{}] [{}]", tenantId, edgeId, key, value);
if (persistToTelemetry) {
tsSubService.saveTimeseries(TimeseriesSaveRequest.builder()
ctx.getTsSubService().saveTimeseries(TimeseriesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edgeId)
.entry(new BooleanDataEntry(key, value))
.callback(new AttributeSaveCallback(tenantId, edgeId, key, value))
.build());
} else {
tsSubService.saveAttributes(AttributesSaveRequest.builder()
ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edgeId)
.scope(AttributeScope.SERVER_SCOPE)
@ -620,32 +617,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
private static class AttributeSaveCallback implements FutureCallback<Void> {
private final TenantId tenantId;
private final EdgeId edgeId;
private final String key;
private final Object value;
AttributeSaveCallback(TenantId tenantId, EdgeId edgeId, String key, Object value) {
this.tenantId = tenantId;
this.edgeId = edgeId;
this.key = key;
this.value = value;
}
@Override
public void onSuccess(@Nullable Void result) {
log.trace("[{}][{}] Successfully updated attribute [{}] with value [{}]", tenantId, edgeId, key, value);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to update attribute [{}] with value [{}]", tenantId, edgeId, key, value, t);
}
}
private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) {
try {
EdgeId edgeId = edge.getId();

36
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -25,6 +25,7 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.data.util.Pair;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils;
@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.AttributesSaveResult;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.limit.LimitedApi;
@ -47,6 +49,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -195,7 +198,7 @@ public abstract class EdgeGrpcSession implements Closeable {
}
startSyncProcess(fullSync);
} else {
syncInProgress = false;
updateSyncInProgress(false);
}
}
if (requestMsg.getMsgType().equals(RequestMsgType.UPLINK_RPC_MESSAGE)) {
@ -241,6 +244,11 @@ public abstract class EdgeGrpcSession implements Closeable {
log.debug("[{}] onConfigurationUpdate [{}]", sessionId, edge);
this.tenantId = edge.getTenantId();
this.edge = edge;
if (!this.edge.getCustomerId().equals(edge.getCustomerId())) {
// do not send edge configuration message on customer update
// message send by separate flow from assign_to or unassing_from customer
return;
}
EdgeUpdateMsg edgeConfig = EdgeUpdateMsg.newBuilder()
.setConfiguration(EdgeMsgConstructorUtils.constructEdgeConfiguration(edge)).build();
ResponseMsg edgeConfigMsg = ResponseMsg.newBuilder()
@ -252,7 +260,7 @@ public abstract class EdgeGrpcSession implements Closeable {
public void startSyncProcess(boolean fullSync) {
if (!syncInProgress) {
log.info("[{}][{}][{}] Staring edge sync process", tenantId, edge.getId(), sessionId);
syncInProgress = true;
updateSyncInProgress(true);
interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync));
} else {
@ -398,6 +406,18 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getAttributesService().save(tenantId, edge.getId(), AttributeScope.SERVER_SCOPE, attributeKvEntry);
}
private void updateSyncInProgress(Boolean value) {
this.syncInProgress = value;
ctx.getTsSubService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(tenantId)
.entityId(edge.getId())
.scope(AttributeScope.SERVER_SCOPE)
.entry(new BooleanDataEntry(DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value))
.callback(new AttributeSaveCallback(tenantId, edge.getId(), DataConstants.EDGE_SYNC_IN_PROGRESS_ATTR_KEY, value))
.build());
}
private void interruptGeneralProcessingOnSync() {
log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", tenantId, edge.getId(), sessionId);
stopCurrentSendDownlinkMsgsTask(true);
@ -766,7 +786,7 @@ public abstract class EdgeGrpcSession implements Closeable {
}
private void markSyncCompletedSendEdgeEventUpdate() {
syncInProgress = false;
updateSyncInProgress(false);
ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId()));
}
@ -969,6 +989,16 @@ public abstract class EdgeGrpcSession implements Closeable {
}
}
}
if (uplinkMsg.getApiKeyUpdateMsgCount() > 0) {
for (ApiKeyUpdateMsg apiKeyUpdateMsg : uplinkMsg.getApiKeyUpdateMsgList()) {
sequenceDependencyLock.lock();
try {
result.add(ctx.getApiKeyProcessor().processApiKeyMsgFromEdge(edge.getTenantId(), edge, apiKeyUpdateMsg));
} finally {
sequenceDependencyLock.unlock();
}
}
}
} catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -146,7 +146,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, API_KEY, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};
@ -412,6 +412,9 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
}
protected boolean isSaveRequired(HasVersion current, HasVersion updated) {
if (current != null) {
current.setVersion(null);
}
updated.setVersion(null);
return !updated.equals(current);
}

105
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyEdgeProcessor.java

@ -0,0 +1,105 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class ApiKeyEdgeProcessor extends BaseApiKeyProcessor implements ApiKeyProcessor {
@Override
public ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg) {
ApiKeyId apiKeyId = new ApiKeyId(new UUID(apiKeyUpdateMsg.getIdMSB(), apiKeyUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
return switch (apiKeyUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
boolean created = saveOrUpdateApiKey(tenantId, apiKeyId, apiKeyUpdateMsg);
if (created) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_CREATED);
}
}
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteApiKey(tenantId, edge, apiKeyId);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(apiKeyUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
ApiKeyId apiKeyId = new ApiKeyId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(edgeEvent.getTenantId(), apiKeyId);
if (apiKey != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
case DELETED -> {
ApiKeyUpdateMsg apiKeyUpdateMsg = EdgeMsgConstructorUtils.constructApiKeyDeleteMsg(apiKeyId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addApiKeyUpdateMsg(apiKeyUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.API_KEY;
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/ApiKeyProcessor.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.apikey;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface ApiKeyProcessor extends EdgeProcessor {
ListenableFuture<Void> processApiKeyMsgFromEdge(TenantId tenantId, Edge edge, ApiKeyUpdateMsg apiKeyUpdateMsg);
}

63
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/apikey/BaseApiKeyProcessor.java

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.apikey;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.ApiKeyId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseApiKeyProcessor extends BaseEdgeProcessor {
protected boolean saveOrUpdateApiKey(TenantId tenantId, ApiKeyId apiKeyId, ApiKeyUpdateMsg apiKeyUpdateMsg) {
boolean isCreated = false;
try {
ApiKey apiKey = JacksonUtil.fromString(apiKeyUpdateMsg.getEntity(), ApiKey.class, true);
if (apiKey == null) {
throw new RuntimeException("[{" + tenantId + "}] apiKeyUpdateMsg {" + apiKeyUpdateMsg + " } cannot be converted to apiKey");
}
ApiKey existingApiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (existingApiKey == null) {
apiKey.setCreatedTime(Uuids.unixTimestamp(apiKeyId.getId()));
isCreated = true;
}
apiKey.setId(apiKeyId);
edgeCtx.getApiKeyService().saveApiKey(tenantId, apiKey, apiKey.getValue(), false);
} catch (Exception e) {
log.error("[{}] Failed to process apiKey update msg [{}]", tenantId, apiKeyUpdateMsg, e);
throw e;
}
return isCreated;
}
protected void deleteApiKey(TenantId tenantId, Edge edge, ApiKeyId apiKeyId) {
ApiKey apiKey = edgeCtx.getApiKeyService().findApiKeyById(tenantId, apiKeyId);
if (apiKey != null) {
edgeCtx.getApiKeyService().deleteApiKey(tenantId, apiKey, false);
pushEntityEventToRuleEngine(tenantId, edge, apiKey, TbMsgType.ENTITY_DELETED);
}
}
}

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java

@ -23,6 +23,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.ApiKeyUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
@ -42,6 +44,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.List;
import java.util.UUID;
@Slf4j
@ -135,6 +138,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso
if (userCredentialsByUserId != null) {
builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId));
}
List<ApiKey> apiKeys = edgeCtx.getApiKeyService().findApiKeysByUserId(edgeEvent.getTenantId(), userId);
for (ApiKey apiKey : apiKeys) {
builder.addApiKeyUpdateMsg(EdgeMsgConstructorUtils.constructApiKeyUpdatedMsg(msgType, apiKey));
}
return builder.build();
}
}

4
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -97,6 +97,10 @@ public abstract class AbstractTbEntityService {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
protected <I extends EntityId> I getOrEmptyId(I entityId, EntityType entityType) {
return entityId == null ? emptyId(entityType) : entityId;
}
protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) {
if (vcService != null) {
return vcService.autoCommit(user, entityId);

3
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbLogEntityActionService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -60,7 +61,7 @@ public class DefaultTbLogEntityActionService implements TbLogEntityActionService
}
@Override
public <E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, I entityId, E entity,
public <E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity,
CustomerId customerId, ActionType actionType,
User user, Exception e, Object... additionalInfo) {
if (user != null) {

7
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.job.Job;
import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -360,10 +361,12 @@ public class EntityStateSourcingListener {
jobManager.onJobUpdate(job);
ComponentLifecycleEvent event;
if (job.getResult().getCancellationTs() > 0) {
if (job.getStatus() == JobStatus.CANCELLED) {
event = ComponentLifecycleEvent.STOPPED;
} else if (job.getResult().getGeneralError() != null) {
} else if (job.getStatus() == JobStatus.FAILED) {
event = ComponentLifecycleEvent.FAILED;
} else if (job.getStatus() == JobStatus.COMPLETED) {
event = ComponentLifecycleEvent.UPDATED;
} else {
return;
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/TbLogEntityActionService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy;
import jakarta.validation.constraints.NotNull;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@ -37,7 +38,7 @@ public interface TbLogEntityActionService {
<E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId,
ActionType actionType, User user, Object... additionalInfo);
<E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId,
<E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, @NotNull I entityId, E entity, CustomerId customerId,
ActionType actionType, User user, Exception e,
Object... additionalInfo);

92
application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java

@ -15,10 +15,22 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -31,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@ -42,8 +60,13 @@ import java.util.Set;
@RequiredArgsConstructor
public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService {
private static final int TIMEOUT = 20;
private final CalculatedFieldService calculatedFieldService;
@Autowired(required = false)
private TbelInvokeService tbelInvokeService;
@Override
public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException {
ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
@ -89,6 +112,75 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
}
}
@Override
public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
tenantId,
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ObjectUtils.firstNonNull(ExceptionUtils.getRootCause(e), e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private static long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
}
private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) {
if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) {
throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited.");

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -35,4 +36,6 @@ public interface TbCalculatedFieldService {
void delete(CalculatedField calculatedField, SecurityUser user);
JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams);
}

67
application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/DefaultTbTenantProfileService.java

@ -15,21 +15,28 @@
*/
package org.thingsboard.server.service.entitiy.tenant.profile;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.queue.TbQueueService;
import java.util.List;
import static org.thingsboard.server.common.data.EntityType.TENANT_PROFILE;
@Slf4j
@Service
@TbCoreComponent
@ -41,18 +48,62 @@ public class DefaultTbTenantProfileService extends AbstractTbEntityService imple
private final TbTenantProfileCache tenantProfileCache;
@Override
public TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException {
TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile));
tenantProfileCache.put(savedTenantProfile);
public TenantProfile
save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException {
ActionType actionType = tenantProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
TenantProfile savedTenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(tenantId, tenantProfile));
tenantProfileCache.put(savedTenantProfile);
logEntityActionService.logEntityAction(tenantId, savedTenantProfile.getId(), savedTenantProfile, null,
actionType, user);
List<TenantId> tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId());
tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile);
List<TenantId> tenantIds = tenantService.findTenantIdsByTenantProfileId(savedTenantProfile.getId());
tbQueueService.updateQueuesByTenants(tenantIds, savedTenantProfile, oldTenantProfile);
return savedTenantProfile;
} catch (ThingsboardException e) {
log.debug("Failed to save tenant profile because ThingsboardException [{}]", tenantProfile, e);
logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e);
throw e;
} catch (DataValidationException e) {
log.debug("Failed to save tenant profile because data validation [{}]", tenantProfile, e);
logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e);
throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} catch (Exception e) {
log.debug("Failed to save tenant profile because Exception [{}]", tenantProfile, e);
logEntityActionService.logEntityAction(tenantId, getOrEmptyId(tenantProfile.getId(), TENANT_PROFILE), tenantProfile, actionType, user, e);
throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL);
}
return savedTenantProfile;
}
@Override
public void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException {
tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId());
public void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException {
ActionType actionType = ActionType.DELETED;
try {
tenantProfileService.deleteTenantProfile(tenantId, tenantProfile.getId());
logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, null, actionType, user, e);
throw e;
}
}
@Override
public TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException {
ActionType actionType = ActionType.UPDATED;
try {
TenantProfile savedTenantProfile = tenantProfileService.setDefaultTenantProfile(tenantId, tenantProfile.getId());
logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), savedTenantProfile, null, actionType, user);
return savedTenantProfile;
} catch (DataValidationException e) {
log.debug("Failed to set default tenant profile due to data validation [{}]", tenantProfile, e);
logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e);
throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} catch (Exception e) {
log.debug("Failed to set default tenant profile [{}]", tenantProfile, e);
logEntityActionService.logEntityAction(tenantId, tenantProfile.getId(), tenantProfile, actionType, user, e);
throw new ThingsboardException(e.getMessage(), e, ThingsboardErrorCode.GENERAL);
}
}
}

8
application/src/main/java/org/thingsboard/server/service/entitiy/tenant/profile/TbTenantProfileService.java

@ -15,13 +15,17 @@
*/
package org.thingsboard.server.service.entitiy.tenant.profile;
import jakarta.validation.constraints.NotNull;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.User;
public interface TbTenantProfileService {
TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile) throws ThingsboardException;
TenantProfile save(TenantId tenantId, TenantProfile tenantProfile, TenantProfile oldTenantProfile, User user) throws ThingsboardException;
void delete(TenantId tenantId, TenantProfile tenantProfile) throws ThingsboardException;
void delete(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException;
TenantProfile setDefaultTenantProfile(TenantId tenantId, @NotNull TenantProfile tenantProfile, User user) throws ThingsboardException;
}

6
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -135,6 +135,10 @@ public class InstallScripts {
return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
}
public Path getWidgetBundlesDir() {
return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
}
public String getDataDir() {
if (!StringUtils.isEmpty(dataDir)) {
if (!Paths.get(this.dataDir).toFile().isDirectory()) {
@ -207,7 +211,7 @@ public class InstallScripts {
public void loadSystemWidgets() {
log.info("Loading system widgets");
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
Path widgetBundlesDir = getWidgetBundlesDir();
try (Stream<Path> dirStream = listDir(widgetBundlesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {

1
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -54,6 +54,7 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
public void upgradeDatabase() {
log.info("Updating schema...");
loadSql(getSchemaUpdateFile("basic"));
loadSql(getSchemaUpdateFile("lts"));
log.info("Schema updated.");
}

92
application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java

@ -15,15 +15,20 @@
*/
package org.thingsboard.server.service.job;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import jakarta.annotation.Nullable;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.JobId;
import org.thingsboard.server.common.data.id.TenantId;
@ -33,7 +38,10 @@ import org.thingsboard.server.common.data.job.JobStatus;
import org.thingsboard.server.common.data.job.JobType;
import org.thingsboard.server.common.data.job.task.Task;
import org.thingsboard.server.common.data.job.task.TaskResult;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.gen.transport.TransportProtos.TaskProto;
@ -50,7 +58,10 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -65,6 +76,8 @@ public class DefaultJobManager implements JobManager {
private final Map<JobType, JobProcessor> jobProcessors;
private final Map<JobType, TbQueueProducer<TbProtoQueueMsg<TaskProto>>> taskProducers;
private final ExecutorService executor;
private final ConcurrentHashMap<JobId, TbCallback> finishCallbacks = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupScheduler;
public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService,
TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig,
@ -76,6 +89,8 @@ public class DefaultJobManager implements JobManager {
this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity()));
this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer));
this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass());
this.cleanupScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("job-callback-cleanup");
this.cleanupScheduler.scheduleWithFixedDelay(this::cleanupStaleCallbacks, 1, 1, TimeUnit.HOURS);
}
@Override
@ -84,6 +99,25 @@ public class DefaultJobManager implements JobManager {
return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor);
}
@Override
public ListenableFuture<Job> submitJob(Job job, TbCallback finishCallback) {
ListenableFuture<Job> saveFuture = submitJob(job);
if (finishCallback != null) {
Futures.addCallback(saveFuture, new FutureCallback<>() {
@Override
public void onSuccess(Job savedJob) {
finishCallbacks.put(savedJob.getId(), finishCallback);
}
@Override
public void onFailure(Throwable t) {
finishCallback.onFailure(t);
}
}, MoreExecutors.directExecutor());
}
return saveFuture;
}
@Override
public void onJobUpdate(Job job) {
JobStatus status = job.getStatus();
@ -109,6 +143,35 @@ public class DefaultJobManager implements JobManager {
}
}
@EventListener
public void onJobUpdateEvent(ComponentLifecycleMsg event) {
EntityId entityId = event.getEntityId();
if (entityId.getEntityType() != EntityType.JOB) {
return;
}
ComponentLifecycleEvent lifecycleEvent = event.getEvent();
if (!lifecycleEvent.equals(ComponentLifecycleEvent.STOPPED) &&
!lifecycleEvent.equals(ComponentLifecycleEvent.FAILED) &&
!lifecycleEvent.equals(ComponentLifecycleEvent.UPDATED)) {
return;
}
JobId jobId = new JobId(entityId.getId());
TbCallback callback = finishCallbacks.remove(jobId);
if (callback == null) {
return;
}
executor.execute(() -> {
try {
Job job = jobService.findJobById(event.getTenantId(), jobId);
invokeFinishCallback(job, callback);
} catch (Throwable e) {
log.error("[{}] Failed to invoke finish callback", jobId, e);
callback.onFailure(e);
}
});
}
private void processJob(Job job) {
TenantId tenantId = job.getTenantId();
JobId jobId = job.getId();
@ -195,12 +258,41 @@ public class DefaultJobManager implements JobManager {
});
}
private void invokeFinishCallback(@Nullable Job job, TbCallback callback) {
if (job == null) {
callback.onFailure(new RuntimeException("Job not found"));
} else if (job.getStatus() == JobStatus.COMPLETED) {
callback.onSuccess();
} else {
callback.onFailure(new RuntimeException(job.getError()));
}
}
private void cleanupStaleCallbacks() {
finishCallbacks.entrySet().removeIf(entry -> {
JobId jobId = entry.getKey();
try {
Job job = jobService.findJobById(TenantId.SYS_TENANT_ID, jobId);
if (job == null || job.getStatus().isOneOf(JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED)) {
invokeFinishCallback(job, entry.getValue());
return true;
}
return false;
} catch (Throwable e) {
log.error("[{}] Failed to cleanup stale callback", jobId, e);
entry.getValue().onFailure(e);
return true;
}
});
}
private JobProcessor getJobProcessor(JobType jobType) {
return jobProcessors.get(jobType);
}
@PreDestroy
private void destroy() {
cleanupScheduler.shutdownNow();
executor.shutdownNow();
}

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<AssetProfileId> toRemove = assetProfilesMap.values().stream()
.filter(assetProfile -> assetProfile.getTenantId().equals(tenantId))
.map(AssetProfile::getId)
.collect(Collectors.toSet());
assetProfilesMap.keySet().removeAll(toRemove);
assetsMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
assetProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<DeviceProfileId> toRemove = deviceProfilesMap.values().stream()
.filter(deviceProfile -> deviceProfile.getTenantId().equals(tenantId))
.map(DeviceProfile::getId)
.collect(Collectors.toSet());
deviceProfilesMap.keySet().removeAll(toRemove);
devicesMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
deviceProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

145
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -15,13 +15,15 @@
*/
package org.thingsboard.server.service.query;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.KvUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
@ -30,11 +32,17 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery;
@ -59,11 +67,13 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@ -253,7 +263,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
if (isAttributes) {
Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size());
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope))));
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope))));
attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
if (CollectionUtils.isEmpty(lists)) {
return Collections.emptyList();
@ -274,4 +284,135 @@ public class DefaultEntityQueryService implements EntityQueryService {
}, dbCallbackExecutor);
}
@Override
public ListenableFuture<AvailableEntityKeysV2> findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query,
boolean includeTimeseries, boolean includeAttributes,
Set<AttributeScope> scopes, boolean includeSamples) {
if (!includeTimeseries && !includeAttributes) {
return Futures.immediateFailedFuture(
new IllegalArgumentException("At least one of 'includeTimeseries' or 'includeAttributes' must be true"));
}
return Futures.transformAsync(findEntityIdsByQueryAsync(securityUser, query), ids -> {
if (ids.isEmpty()) {
return immediateFuture(new AvailableEntityKeysV2(
Collections.emptySet(),
includeTimeseries ? Collections.emptyList() : null,
includeAttributes ? Collections.emptyMap() : null));
}
TenantId tenantId = securityUser.getTenantId();
Set<EntityType> entityTypes = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet());
var tsFuture = includeTimeseries ? fetchTimeseriesKeys(tenantId, ids, includeSamples) : null;
Set<AttributeScope> effectiveScopes = includeAttributes
? resolveAttributeScopes(scopes, entityTypes) : Collections.emptySet();
var attrFutures = effectiveScopes.stream()
.map(scope -> fetchAttributeKeys(tenantId, ids, scope, includeSamples))
.toList();
return assembleResult(entityTypes, tsFuture, attrFutures);
}, dbCallbackExecutor);
}
private ListenableFuture<List<EntityId>> findEntityIdsByQueryAsync(SecurityUser securityUser, EntityDataQuery query) {
return Futures.transform(entityService.findEntityDataByQueryAsync(securityUser.getTenantId(), securityUser.getCustomerId(), query),
page -> page.getData().stream()
.map(EntityData::getEntityId)
.toList(),
dbCallbackExecutor);
}
private static Set<AttributeScope> resolveAttributeScopes(Set<AttributeScope> requestedScopes, Set<EntityType> entityTypes) {
boolean hasDevices = entityTypes.contains(EntityType.DEVICE);
Set<AttributeScope> scopes;
if (CollectionUtils.isNotEmpty(requestedScopes)) {
scopes = requestedScopes;
} else { // auto-determine scopes
scopes = hasDevices
? Set.of(AttributeScope.SERVER_SCOPE, AttributeScope.CLIENT_SCOPE, AttributeScope.SHARED_SCOPE)
: Collections.singleton(AttributeScope.SERVER_SCOPE);
}
// Non-device entities only support SERVER_SCOPE
if (!hasDevices) {
return scopes.contains(AttributeScope.SERVER_SCOPE)
? Collections.singleton(AttributeScope.SERVER_SCOPE)
: Collections.emptySet();
}
return scopes;
}
private ListenableFuture<List<KeyInfo>> fetchTimeseriesKeys(TenantId tenantId, List<EntityId> entityIds, boolean includeSamples) {
if (includeSamples) {
return Futures.transform(
timeseriesService.findLatestByEntityIdsAsync(tenantId, entityIds),
entries -> toKeyInfos(entries, true),
dbCallbackExecutor);
}
return Futures.transform(
timeseriesService.findAllKeysByEntityIdsAsync(tenantId, entityIds),
keys -> keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList(),
dbCallbackExecutor);
}
private ListenableFuture<Map.Entry<AttributeScope, List<KeyInfo>>> fetchAttributeKeys(
TenantId tenantId, List<EntityId> entityIds, AttributeScope scope, boolean includeSamples) {
if (includeSamples) {
return Futures.transform(
attributesService.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope),
entries -> Map.entry(scope, toKeyInfos(entries, true)),
dbCallbackExecutor);
}
return Futures.transform(
attributesService.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope),
keys -> Map.entry(scope, keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList()),
dbCallbackExecutor);
}
private ListenableFuture<AvailableEntityKeysV2> assembleResult(
Set<EntityType> entityTypes,
ListenableFuture<List<KeyInfo>> tsFuture,
List<ListenableFuture<Map.Entry<AttributeScope, List<KeyInfo>>>> attrFutures) {
var allAttrFuture = attrFutures.isEmpty()
? immediateFuture(List.<Map.Entry<AttributeScope, List<KeyInfo>>>of())
: Futures.allAsList(attrFutures);
List<ListenableFuture<?>> allFutures = new ArrayList<>();
if (tsFuture != null) {
allFutures.add(tsFuture);
}
allFutures.add(allAttrFuture);
var finalTsFuture = tsFuture;
return Futures.whenAllComplete(allFutures)
.call(() -> {
List<KeyInfo> tsKeys = finalTsFuture != null ? Futures.getDone(finalTsFuture) : null;
Map<AttributeScope, List<KeyInfo>> attrMap = attrFutures.isEmpty() ? null : new TreeMap<>();
if (attrMap != null) {
for (var entry : Futures.getDone(allAttrFuture)) {
attrMap.put(entry.getKey(), entry.getValue());
}
}
return new AvailableEntityKeysV2(entityTypes, tsKeys, attrMap);
}, dbCallbackExecutor);
}
private static List<KeyInfo> toKeyInfos(List<? extends KvEntry> entries, boolean includeSamples) {
return entries.stream()
.map(e -> new KeyInfo(e.getKey(), includeSamples ? toKeySample(e) : null))
.sorted(Comparator.comparing(KeyInfo::key))
.toList();
}
private static KeySample toKeySample(KvEntry entry) {
long ts = entry instanceof TsKvEntry tsKv ? tsKv.getTs()
: entry instanceof AttributeKvEntry attr ? attr.getLastUpdateTs()
: 0;
JsonNode value = entry.getDataType() == DataType.JSON
? JacksonUtil.toJsonNode(entry.getJsonValue().get())
: JacksonUtil.valueToTree(entry.getValue());
return new KeySample(ts, value);
}
}

7
application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java

@ -23,11 +23,14 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AvailableEntityKeys;
import org.thingsboard.server.common.data.query.AvailableEntityKeysV2;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set;
public interface EntityQueryService {
long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query);
@ -41,4 +44,8 @@ public interface EntityQueryService {
ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope);
ListenableFuture<AvailableEntityKeysV2> findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query,
boolean includeTimeseries, boolean includeAttributes,
Set<AttributeScope> scopes, boolean includeSamples);
}

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -51,7 +51,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@ -91,9 +90,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -86,7 +86,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -179,9 +178,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, null, null, null);
super(actorContext, null, null, null, null, null, null, null, null);
this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory);
this.queueFactory = tbCoreQueueFactory;

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -46,7 +46,6 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService;
@ -84,9 +83,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

35
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -26,7 +26,6 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -47,7 +46,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
@ -75,7 +73,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
@ -166,7 +163,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
tenantProfileCache.evict(tenantProfileId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
calculatedFieldCache.handleTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
@ -179,7 +175,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantUpdate(tenantId);
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete(tenantId);
calculatedFieldCache.evictOwner(tenantId);
partitionService.removeTenant(tenantId);
}
}
@ -187,45 +182,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
deviceProfileCache.evict(tenantId, new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(tenantId, new DeviceId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
apiUsageStateService.onApiUsageStateUpdate(tenantId);
} else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictOwner(componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.CALCULATED_FIELD.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.CREATED) {
calculatedFieldCache.addCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.UPDATED) {
calculatedFieldCache.updateCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -45,12 +44,11 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save